Introduction to unit test: services

June 1, 2014    unit test

Testing a service is not much more difficult than testing a filter, in fact, the same rules applies. The difficulty comes on what you do with your service. It is not the same thing testing a service that holds data (a wizard for example) than a service that does RESTful stuff + cache. I plan to write more testing examples on the future but for this article we are going to set the basics :)

So what are we going to do on this service? Well taking advantage of my toastr library, I thought about a logger service. What can we do with it? Let me think… Right, we could use it to log to the dev console using its log, debug and error functions and also optionally pop up a toast with proper colors to match those functions. Sounds good to me, let’s go:

describe('factory: Logger', function() {
  var logger, toastr;
  
  beforeEach(inject(function(_logger_) {
    logger = _logger_;
  }));
  
});

Well, we created a variable to hold our logger service and also one to hold the toastr service. Then we injected the logger and saved it. What should we do with the toastr service?

This is a common pain point for new users. I bet that the half of you would have this question here: How can I test that the toast is on the screen? The answer is: You don’t care. This is a UNIT test, that means that you’re testing that concrete unit and you shouldn’t care about its dependencies. If it is another service you made, you would have tests for it and if it is a third party dependency, it has its own tests (or it should :P). So in this case, the toastr service is already tested so you don’t have to care.

Alright, having that in mind, the common path here is to spy the functions we are going to use from the toastr service and also the functions from the console. I am going to show you two different ways:

The previous code would end like this:

describe('factory: Logger', function() {
  var logger, toastr;
  
  beforeEach(module('app', function($provide) {
    toastr = {};
    
    toastr.info = jasmine.createSpy();
    toastr.warning = jasmine.createSpy();
    toastr.error = jasmine.createSpy();
    
    $provide.value('toastr', toastr);
  }));
  
  beforeEach(inject(function(_logger_) {
    logger = _logger_;
    
    spyOn(console, 'log');
    spyOn(console, 'debug');
    spyOn(console, 'error');
  }));  
});

For the console, we are going to stick on spyOn from jasmine as we did on a previous article, but for toastr we did something completely new. We replaced the toastr service with one we made right here, in other words, a complete mock.

Basically, when we loaded the app module (we do this on the app module because it is where the service is created or added as a dependency) we created a new toastr object and then we created 3 spies (more on this shortly). After that we just needed to create a new value service that will create/override the toastr one.

If we have a service (of any kind) with a certain name and we after that create another service (of any kind) with that same name, it will be overriden. That is whycreating a simple value service will override the previous one.

If we load the original toastr library on the test, it will be overriden but in this case, we can just ignore the dependency and a new toastr service will be created. In any case we have what we need.

What about those createSpy? They do more or less the same as spyOn. What’s the difference? spyOn is used to spy an existing function and createSpy will create a dummy spied function. Since our new toastr service has none, we can create spied functions from scratch, handy.

Alright, our preparations are done, let’s write a couple of tests:

it('should log using the log function but not toast', function() {
  logger.log('Hello');
  expect(console.log).toHaveBeenCalledWith('Hello');
  expect(toastr.info).not.toHaveBeenCalledWith('Hello');
});

it('should log using the log function and also toast', function() {
  logger.log('Foo', true);
  expect(console.log).toHaveBeenCalledWith('Foo');
  expect(toastr.info).toHaveBeenCalledWith('Foo');
});

it('should log using the debug function and also toast', function() {
  logger.log('Foo', 'debug', true);
  expect(console.debug).toHaveBeenCalledWith('Foo');
  expect(toastr.warning).toHaveBeenCalledWith('Foo');
});

it('should log to the debug function but without toast', function() {
  logger.log('Foo', 'debug');
  expect(console.debug).toHaveBeenCalledWith('Foo');
  expect(toastr.info).not.toHaveBeenCalledWith('Foo');
});

it('should log using the error function and also toast', function() {
  logger.log('Bar', 'error', true);
  expect(console.error).toHaveBeenCalledWith('Bar');
  expect(toastr.error).toHaveBeenCalledWith('Bar');
});

it('should log to the error function but without toast', function() {
  logger.log('Baz', 'error');
  expect(console.error).toHaveBeenCalledWith('Baz');
  expect(toastr.error).not.toHaveBeenCalledWith('Baz');
});

it('should fallback to the log function if it is not valid', function() {
  logger.log('Not valid', 'emergency', true);
  expect(console.log).toHaveBeenCalledWith('Not valid');
  expect(toastr.info).toHaveBeenCalledWith('Not valid');
});

Here we are testing the different combinations of our logger. As you can see, it has 3 parameters, one for the message, one for the log type and one boolean for our toastr popup. The type parameter is optional and will use log by default. Also I provided a fallback option to the library.

The final result of the service is:

angular.module('app', ['toastr']).factory('logger', function(toastr) {
  
  var types = {
    'log': 'info',
    'debug': 'warning',
    'error': 'error'
  };
  
  var log = function(message, type, toast) {
    if (typeof type == "boolean") {
      toast = type;
    }
    
    if (!types.hasOwnProperty(type)) {
      type = 'log';
    }
    
    console[type](message);
    
    if (toast) {
      toastr[types[type]](message);
    }
  };
  
  return {
    log: log
  };
  
});

I mapped the console functions to the toastr function that has the most appropiated colors. And the log function is easy, we just log and show a popup if needed.

Even when we used a factory here, testing a service is not different.

You can see it live here.

comments powered by Disqus