angular / angular.js

AngularJS - HTML enhanced for web apps!
https://angularjs.org
MIT License
58.83k stars 27.51k forks source link

Docs: how to unit test a service #12894

Open skolsuper opened 9 years ago

skolsuper commented 9 years ago

The developer guide docs cover specifically how to unit test a controller, how to test a directive, and how to test a transclusion directive, but not how to test a service. I have been clicking around stackoverflow for about 4 hours now and have found 100 different ways of doing this, none of which have worked. What is the canonical way of unit testing e.g. "myLoginService"?

edit: in angular 1.3.19

skolsuper commented 9 years ago

I should add that I need to mock a few dependencies for the service.

benhalverson commented 9 years ago

This is from the yeoman generator-angular https://github.com/yeoman/generator-angular#service

'use strict';

describe('Service: myService', function () {

  // load the service's module
  beforeEach(module('angulartestApp'));

  // instantiate service
  var myService;
  beforeEach(inject(function (_myService_) {
    myService = _myService_;
  }));

  it('should do something', function () {
    expect(!!myService).toBe(true);
  });

});
skolsuper commented 9 years ago

Thanks Ben, although that doesn't describe how to mock the dependencies of myService for the test. I ended up doing this:

describe('Service: myLoginService', function () {

  var myLoginService;

  beforeEach(module('Auth'));
  beforeEach(module(function ($provide) {
    $provide.service('$window', function () {
      function LocalStorage() {
        var item = null;
        this.getItem = function () {
          return item;
        };
        this.setItem = function (input) {
          item = input;
        };
        this.removeItem = function () {
          item = null;
        };
      }
      this.localStorage = new LocalStorage();
    });
    $provide.service('$log', function () {
      this.log = jasmine.createSpy('log');
    });
    $provide.service('$location', function () {
      this.path = jasmine.createSpy('path');
    });
    $provide.constant('loginUrl', '/test/login/');
  }));

  it('should deal with login functionality', inject(function ($injector, $httpBackend, $window, $log, $location) {

    myLoginService = $injector.get('myLoginService');
    // tests go here
  }));
});

The purpose of raising this issue wasn't really to get advice (OK, it was a bit), but to point out a hole in the docs, in my opinion. It seems to me that services are a prime candidate for unit testing, whereas well-designed controllers should really be as slim as possible, simply connecting various services and making properties available in the scope. I think if there is a whole section specifically on unit-testing controllers, there should be a section on unit-testing services. If someone can answer with authority a canonical method of doing it, I will happily submit a PR to update the docs.

Narretz commented 9 years ago

What does your service look like anyway?

skolsuper commented 9 years ago

Something like:

angular.module('myApp')
  .factory('myService', function ($window, $http, $log, ...) {
    return {
      foo: foo,
      bar: bar,
      baz: baz
    };
    function foo () { ... // and so on
});

It uses window.localStorage to emulate a session for a REST API which uses JWTs for auth, so the user doesn't have to log in each time they visit.