Craft Academy

Läs mer om oss

Testing Google Maps

av Thomas Ochman


In one of the Craft Academy week labs that we challenge our students to solve, we make use of a google map on the landing page. One of the many features they implement while working on the application is to query the visitors current location and center the map on those coordinates.

We use Cucumber as our primary framework for acceptance tests and aim to cover as many of our features as possible with tests. Testing the map functionality proved to be rather difficult.

Loading the map

Displaying a google map on a web page is pretty straight forward using the Google native Map API. In our week labs however, we tend to point our learners to Gmaps.js - a self proclaimed 'less pain and more fun' Google Maps API. I have to give it to the good people behind Gmaps.js. It IS less complicated then the native API provided by Google, to a point where it becomes ridiculously easy.

The view template in haml:

#map{style: 'width: 800px; height: 400px'}

%script(src="https://maps.googleapis.com/maps/api/js?libraries=places&callback=initiateMap" async defer)
= javascript_include_tag 'gmaps'
= javascript_include_tag 'landing_page'

We use two .js files apart from loading the google api. The gmaps.js file contains the API wrapper and can be downloaded from the Gmaps.js site. Finally, the landing_page.js file contains our own scripts - including the initiateMap function that we use as a callaback once we have loaded the google maps js.

landing_page.js

function initiateMap () {  
    map = GMaps({
        div: '#map',
        zoom: 12,
        lat: 57.7089,
        lng: 11.9746
    });
};

That's all we need in order to display a 800 x 400 px map on the web page. Pretty straight forward, right?

Okay, so how can we test this in Cucumber?

Let's have a look at a possible scenario for the map.

Scenario: Centering the map on user location  
    Given I am on the landing page
    And the map has been loaded
    # ...rest of our scenario

The step definition for the Given I am on the landing page step is pretty basic:

Given(/^I am on the landing page$/) do  
  visit root_path
end  

What about the rest?

Using the right tool for the the task

First of all we are using JavaScript to get the map on the page so our usual Cucumber setup will not be enough. We have to make sure that our framework is using a javascript enabled driver. My choice is Poltergeist - a javascript driver for Capybara. It allows us to run Capybara tests on a headless WebKit browser, provided by PhantomJS. Sounds complicated? Well, the setup is rather easy and following the instructions should get you up and running pretty fast.

Poltergeist requires PhantomJS and the easiest way to install it on OsX is through brew.

$ brew install phantomjs

You also have to add the poltergeist gem to your Gemfile, bundle and set up Cucumber and Capybara to use it. We can do that by adding the following settings to our env.rb file:

features/support/env.rb

require 'capybara/poltergeist'

Capybara.register_driver :poltergeist do |app|  
  Capybara::Poltergeist::Driver.new(app, js_error: false)
end  
Capybara.javascript_driver = :poltergeist

Now, we can tell Cucumber to run the javascript_driver by adding the @javascript hook to our scenarios.

Adding the following step will now make sense:

And(/^(?:I expect a Google map to load|the map has been loaded)$/) do  
  sleep(0.1) until page.evaluate_script('$.active') == 0
  expect(page).to have_css '#map .gm-style'
end  

This will tell our driver to wait until the Google maps api has responded and the js file loaded. We know that if everything is correctly configured, the .gm-style class is added to the element where we intend to load the map.

So let's try to modify our scenario to test for centering the map on the users current location

Scenario: Centering the map on user location  
    Given I am on the landing page
    And the map has been loaded
    And my location is set to "57.7088700" lat and "11.9745600" lng
    Then the center of the map should be approximately "57.7088700" lat and "11.9745600" lng

Here comes the "problem": We can not perform geolocation in test environment so we need to condition geocoding to only be performed in production. When we run tests we want to use preset values for latitude and longitude.

Our js implementation can look something like this:

landing_page.js

function initiateMap () {  
    map = GMaps({
        div: '#map',
        zoom: 12,
        lat: 57.7089,
        lng: 11.9746
    });
    performGeolocation();
};

function performGeolocation(lat, lng) {  
  var latitude;
  var longitude;
  var testing_env = $('#map').data().testEnv;
  if (testing_env === false) {
      GMaps.geolocate({
          success: function (position) {
              latitude = position.coords.latitude;
              longitude = position.coords.longitude;
              map.setCenter(latitude, longitude);
          },
          error: function (error) {
              alert('Geolocation failed: ' + error.message);
          },
          not_supported: function () {
              alert('Your browser does not support geolocation');
          }
      });
  } else {
      latitude = lat || 57.690123; //or whatever value you find appropriate 
      longitude = lng || 11.950632; // same here
      map.setCenter(latitude, longitude);
  }
}

Consequently, the HAML markup in our template needs to be updated to include the dataSet for the Rails environment:

#map{style: 'width: 800px; height: 400px', data: {test: {env: "#{Rails.env.test?}"}}}

So how can our step definitions be formulated? We have to bear in mind that geolocation is not an very exact feature in Google maps and there will always be an offset. I've found this implementation to be good enough for us:

Then(/^the center of the map should be approximately "([^"]*)" lat and "([^"]*)" lng$/) do |lat, lng|  
  ACCEPTED_OFFSET = 0.2
  center_lat = page.evaluate_script('map.getCenter().lat();')
  center_lng = page.evaluate_script('map.getCenter().lng();')
  expect(center_lat).to be_within(ACCEPTED_OFFSET).of(lat.to_f)
  expect(center_lng).to be_within(ACCEPTED_OFFSET).of(lng.to_f)
end  

Now our scenario should pass. The map implementation cam be improved of course, but it's okay as a start. In future iterations we will be displaying more content on the map using markers and I'll be happy to share our findings about how to test that in separate posts.

Thx for reading.


author image

Thomas Ochman