At Public Lab we have several Javascript repositories that compile using grunt. Most of these end up included in our Ruby on Rails app using npm packages, which means sometimes in order to make a change on the website I have to download the JS repository, make the changes (adding new functions, error catching), bump the version number, ask someone to publish the changes, then use npm in the Rails app to update the package.

Last week one of the changes I had to make was with the Google Maps API we use: I had to switch from the server-side version to the client-side javascript version. I made the appropriate changes, made sure everything was working smoothly… and then discovered that the Jasmine tests were throwing a warning. Since it was a warning and not an error we tried ignoring it, but the API change was also causing problems in other repositories that used the main one so I realized I had to figure out a fix.

Testing Functions That Use The Google Maps API

This is what I saw when I ran tests in one of the JS repositories. ReferenceError: Can't find variable: google

And this is what it caused in one of our other repositories:

I spent a lot of time dissecting each line of code and tests, making changes, trying different things. I’d spend hours working on something I thought could fix it only to find out that nope, it wasn’t going to work. At first I was just trying to get the API functioning in the tests, I couldn’t figure out why the google object didn’t exist after calling the api key.

My first epiphany was when I looked at the actual google API code and saw this:

window.google = window.google || {};
google.maps = google.maps || {};

This is a problem because in headless testing there is no window object! I tried various ways of declaring a empty window object and then letting the api continue as usual but that didn’t work. I finally realized that the best way to solve this problem – though possibly time-consuming to set up – was to mock out the google API. This would also allow me to add in specific tests for the functions that used geocoding, which we didn’t have. And again, at first I tried this in various ways with no success. I couldn’t get the helper file to work, I couldn’t call a function, I had to figure out how to get a constant variable to be passed into tests that could be modified by the app.

Google searches for testing the google maps api had among the results this page, which gave me a simple mock object to build on. After some trial and error I wrote a function for Geocoder because that’s the one my function was going to be calling. In order to do this I had to deconstruct this function and how it works, because it takes in a string and a callback function, then executes the function with the results and status.

google = {
  maps: {
    places: {
      AutocompleteService: function() {},
      PlacesServiceStatus: {
        INVALID_REQUEST: 'INVALID_REQUEST',
        NOT_FOUND: 'NOT_FOUND',
        OK: 'OK',
        OVER_QUERY_LIMIT: 'OVER_QUERY_LIMIT',
        REQUEST_DENIED: 'REQUEST_DENIED',
        UNKNOWN_ERROR: 'UNKNOWN_ERROR',
        ZERO_RESULTS: 'ZERO_RESULTS',
      }
    },
    Geocoder: function(stringObj, functionToDo) {
      functionToDo = functionToDo || function() {};
      functionToDo(response.results, "OK");
    },
    GeocoderStatus: {
      ERROR: 'ERROR',
      INVALID_REQUEST: 'INVALID_REQUEST',
      OK: 'OK',
      OVER_QUERY_LIMIT: 'OVER_QUERY_LIMIT',
      REQUEST_DENIED: 'REQUEST_DENIED',
      UNKNOWN_ERROR: 'UNKNOWN_ERROR',
      ZERO_RESULTS: 'ZERO_RESULTS',
    },
  }
};

Next I needed a response object to pass in. This is your test data, so this is what you will expect to see when your function works correctly. Change it at will!

response = {
  "results" : [
     {
        "address_components" : [
           {
              "long_name" : "Winnetka",
              "short_name" : "Winnetka",
              "types" : [ "locality", "political" ]
           },
           {
              "long_name" : "New Trier",
              "short_name" : "New Trier",
              "types" : [ "administrative_area_level_3", "political" ]
           },
           {
              "long_name" : "Cook County",
              "short_name" : "Cook County",
              "types" : [ "administrative_area_level_2", "political" ]
           },
           {
              "long_name" : "Illinois",
              "short_name" : "IL",
              "types" : [ "administrative_area_level_1", "political" ]
           },
           {
              "long_name" : "United States",
              "short_name" : "US",
              "types" : [ "country", "political" ]
           }
        ],
        "formatted_address" : "Winnetka, IL, USA",
        "geometry" : {
           "bounds" : {
              "northeast" : {
                 "lat" : 42.1282269,
                 "lng" : -87.7108162
              },
              "southwest" : {
                 "lat" : 42.0886089,
                 "lng" : -87.7708629
              }
           },
           "location" : {
              "lat" : function() { return 25.0 },
              "lng" : function() { return 17.0 }
           },
           "location_type" : "APPROXIMATE",
           "viewport" : {
              "northeast" : {
                 "lat" : 42.1282269,
                 "lng" : -87.7108162
              },
              "southwest" : {
                 "lat" : 42.0886089,
                 "lng" : -87.7708629
              }
           }
        },
        "place_id" : "ChIJW8Va5TnED4gRY91Ng47qy3Q",
        "types" : [ "locality", "political" ]
     }
  ],
  "status" : "OK"
}

Finally I had to re-create the Geocoder function using a Jasmine Spy. I used some of the instructions here to help me figure out how to do this – the Jasmine Docs aren’t terribly helpful. It’s spying on the google.maps object we just created, but also creating a Spy Object as the variable geocoder. This is necessary for my code because it’s expecting this object to exist, Now when one of the functions tries to use the geocoder object it will use this one instead of the real google.maps version.

var geocoderSpy;
var geocoder;

function createGeocoder() {
  geocoderSpy = spyOn(google.maps, 'Geocoder');
  geocoder = jasmine.createSpyObj('Geocoder', ['geocode']);
  geocoderSpy.and.returnValue(geocoder);
}

Now in my spec file, after all of that has been initialized and the fixture has been loaded, I can run my tests. I call a fake geocoder function and pass in the response object and whatever status I want to test for. When I call a function from my code – getPlacenameFromCoordinates – I pass in variables that it expects and the callback function that it will execute. It will use the created fake geocoder to process that data, look up the appropriate data in the results object, in this case I expect to see the Country name being saved.

beforeAll(function() {
  createGeocoder();
});

beforeEach(function() {
  var fixture = loadFixtures('index.html');
});

it("Checks if getPlacenameFromCoordinates returns country name for location precision 0", function() {
  geocoder.geocode.and.callFake(function(request, callback) {
    callback(response.results, google.maps.GeocoderStatus.OK);
  });

  var string = '';
  blurredLocation.getPlacenameFromCoordinates(42, 11, 0, function(result){
    string = result.trim();
  });
  expect(string).toBe('USA');
});

Jasmine Helper Files With Grunt

Second problem: moving the mock object to a helper file – why isn’t it working? There was a spec/support/jasmine.json file just as it says in the Jasmine Docs, everything was named correctly, but no files in the helpers/ folder were being loaded no matter how I formatted that functions in the helper file.

My clue for this one was when I changed jasmine.json to only load one of my spec files, but it still loaded all of them. Searching online brought me to this page on stack overflow: If you are using grunt to run jasmine then that data gets added to the Gruntfile.js instead. That explains it all!

So I was able to delete the support/ folder entirely, added my helpers into the Gruntfile.js:

jasmine: {
  src: "src/client/js/*.js",
  options: {
    specs: "spec/javascripts/*spec.js",
    helpers: "spec/helpers/*.js",
    vendor: [
      "node_modules/jquery/dist/jquery.js",
      "dist/Leaflet.BlurredLocation.js",
      "node_modules/jasmine-jquery/lib/jasmine-jquery.js"
    ]
  }
},

Now I was able to move the code into the spec/helpers/google_mock.js file and it loaded automatically! Now my tests are set up correctly, able to mock the geocoder object for testing, and it runs with no errors! It feels good to see those green checkmarks. 🙂