【问题标题】:How do I test an AngularJS service with Jasmine?如何使用 Jasmine 测试 AngularJS 服务?
【发布时间】:2012-10-12 09:37:25
【问题描述】:

(这里有一个相关问题:Jasmine test does not see AngularJS module)

我只想在不引导 Angular 的情况下测试服务。

我看过一些示例和教程,但我不会去任何地方。

我只有三个文件:

  • myService.js:我在其中定义了一个 AngularJS 服务

  • test_myService.js:我在其中为服务定义了 Jasmine 测试。

  • specRunner.html:具有正常 jasmine 配置的 HTML 文件 以及我导入前两个文件和 Jasmine、Angularjs 和 angular-mocks.js 的位置。

这是服务的代码(在我不测试时按预期工作):

var myModule = angular.module('myModule', []);

myModule.factory('myService', function(){

    var serviceImplementation   = {};
    serviceImplementation.one   = 1;
    serviceImplementation.two   = 2;
    serviceImplementation.three = 3;

    return serviceImplementation

});

当我尝试单独测试服务时,我应该能够访问它并检查他们的方法。 我的问题是:如何在不引导 AngularJS 的情况下在我的测试中注入服务?

例如,如何使用 Jasmine 测试服务方法返回的值,如下所示:

describe('myService test', function(){
    describe('when I call myService.one', function(){
        it('returns 1', function(){
            myModule = angular.module('myModule');
                    //something is missing here..
            expect( myService.one ).toEqual(1);
        })

    })

});

【问题讨论】:

    标签: angularjs jasmine


    【解决方案1】:

    问题是在上面的示例中没有调用实例化服务的工厂方法(仅创建模块不会实例化服务)。

    为了实例化服务angular.injector,必须使用定义我们服务的模块调用。然后,我们可以向服务的新注入器对象询问,并且只有在服务最终实例化时才可以。

    这样的工作:

    describe('myService test', function(){
        describe('when I call myService.one', function(){
            it('returns 1', function(){
                var $injector = angular.injector([ 'myModule' ]);
                var myService = $injector.get( 'myService' );
                expect( myService.one ).toEqual(1);
            })
    
        })
    
    });
    

    另一种方法是使用“invoke”将服务传递给函数:

    describe('myService test', function(){
        describe('when I call myService.one', function(){
            it('returns 1', function(){
    
                myTestFunction = function(aService){
                    expect( aService.one ).toEqual(1);
                }
    
                //we only need the following line if the name of the 
                //parameter in myTestFunction is not 'myService' or if
                //the code is going to be minify.
                myTestFunction.$inject = [ 'myService' ];
    
                var myInjector = angular.injector([ 'myModule' ]);
                myInjector.invoke( myTestFunction );
            })
    
        })
    
    });
    

    最后,“正确”的做法是在“beforeEach”茉莉花块中使用“inject”和“module”。 这样做时,我们必须意识到“注入”函数不在标准的 angularjs 包中,而是在 ngMock 模块中,并且它只适用于 jasmine。

    describe('myService test', function(){
        describe('when I call myService.one', function(){
            beforeEach(module('myModule'));
            it('returns 1', inject(function(myService){ //parameter name = service name
    
                expect( myService.one ).toEqual(1);
    
            }))
    
        })
    
    });
    

    【讨论】:

    • 希望看到一个示例,说明您的服务何时具有自己的依赖项(例如 $log)
    • 对不起,我实际上是在寻找这样的东西:stackoverflow.com/q/16565531/295797
    • 在服务需要很多...很多...很多测试的情况下,是否有一种将服务注入beforeEach 的好方法?测试一个数据模型(服务),它拥有大量的全局变量。谢谢,C§
    • 你没有说为什么(3)是'正确的方式'
    • @LeeGee 我认为我们可以称之为“正确”的方式,因为它使用专门用于测试目的的 ngMock AngularJS 模块。
    【解决方案2】:

    虽然上面的答案可能工作得很好(我还没有尝试过:)),但我经常有更多的测试要运行,所以我不会自己注入测试。我将 it() 案例分组到描述块中,并在每个描述块中的 beforeEach() 或 beforeAll() 中运行我的注入。

    Robert 也是正确的,他说您必须使用 Angular $injector 来使测试了解服务或工厂。 Angular 在你的应用程序中也使用这个注入器来告诉应用程序什么是可用的。 但是,它可以在多个地方调用,也可以隐式调用而不是显式调用。您会在下面的示例规范测试文件中注意到,beforeEach() 块隐式调用注入器 以使事物可在测试内部分配。

    回到对事物进行分组并使用前块,这里有一个小例子。我正在制作一个 Cat 服务并且我想对其进行测试,所以我编写和测试服务的简单设置如下所示:

    app.js

    var catsApp = angular.module('catsApp', ['ngMockE2E']);
    
    angular.module('catsApp.mocks', [])
    .value('StaticCatsData', function() {
      return [{
        id: 1,
        title: "Commando",
        name: "Kitty MeowMeow",
        score: 123
      }, {
        id: 2,
        title: "Raw Deal",
        name: "Basketpaws",
        score: 17
      }, {
        id: 3,
        title: "Predator",
        name: "Noseboops",
        score: 184
      }];
    });
    
    catsApp.factory('LoggingService', ['$log', function($log) {
    
      // Private Helper: Object or String or what passed
        // for logging? Let's make it String-readable...
      function _parseStuffIntoMessage(stuff) {
        var message = "";
        if (typeof stuff !== "string") {
          message = JSON.stringify(stuff)
        } else {
          message = stuff;
        }
    
        return message;
      }
    
      /**
       * @summary
       * Write a log statement for debug or informational purposes.
       */
      var write = function(stuff) {
        var log_msg = _parseStuffIntoMessage(stuff);
        $log.log(log_msg);
      }
    
      /**
       * @summary
       * Write's an error out to the console.
       */
      var error = function(stuff) {
        var err_msg = _parseStuffIntoMessage(stuff);
        $log.error(err_msg);
      }
    
      return {
        error: error,
        write: write
      };
    
    }])
    
    catsApp.factory('CatsService', ['$http', 'LoggingService', function($http, Logging) {
    
      /*
        response:
          data, status, headers, config, statusText
      */
      var Success_Callback = function(response) {
        Logging.write("CatsService::getAllCats()::Success!");
        return {"status": status, "data": data};
      }
    
      var Error_Callback = function(response) {
        Logging.error("CatsService::getAllCats()::Error!");
        return {"status": status, "data": data};
      }
    
      var allCats = function() {
        console.log('# Cats.allCats()');
        return $http.get('/cats')
          .then(Success_Callback, Error_Callback);
      }
    
      return {
        getAllCats: allCats
      };
    
    }]);
    
    var CatsController = function(Cats, $scope) {
    
      var vm = this;
    
      vm.cats = [];
    
      // ========================
    
      /**
       * @summary
       * Initializes the controller.
       */
      vm.activate = function() {
        console.log('* CatsCtrl.activate()!');
    
        // Get ALL the cats!
        Cats.getAllCats().then(
          function(litter) {
            console.log('> ', litter);
            vm.cats = litter;
            console.log('>>> ', vm.cats);
          }  
        );
      }
    
      vm.activate();
    
    }
    CatsController.$inject = ['CatsService', '$scope'];
    catsApp.controller('CatsCtrl', CatsController);
    

    规格:猫控制器

    'use strict';
    
    describe('Unit Tests: Cats Controller', function() {
    
        var $scope, $q, deferred, $controller, $rootScope, catsCtrl, mockCatsData, createCatsCtrl;
    
        beforeEach(module('catsApp'));
        beforeEach(module('catsApp.mocks'));
    
        var catsServiceMock;
    
        beforeEach(inject(function(_$q_, _$controller_, $injector, StaticCatsData) {
          $q = _$q_;
          $controller = _$controller_;
    
          deferred = $q.defer();
    
          mockCatsData = StaticCatsData();
    
          // ToDo:
            // Put catsServiceMock inside of module "catsApp.mocks" ?
          catsServiceMock = {
            getAllCats: function() {
              // Just give back the data we expect.
              deferred.resolve(mockCatsData);
              // Mock the Promise, too, so it can run
                // and call .then() as expected
              return deferred.promise;
            }
          };
        }));
    
    
        // Controller MOCK
        var createCatsController;
        // beforeEach(inject(function (_$rootScope_, $controller, FakeCatsService) {
        beforeEach(inject(function (_$rootScope_, $controller, CatsService) {
    
          $rootScope = _$rootScope_;
    
          $scope = $rootScope.$new();
          createCatsController = function() {
              return $controller('CatsCtrl', {
                  '$scope': $scope,
                  CatsService: catsServiceMock
              });    
          };
        }));
    
        // ==========================
    
        it('should have NO cats loaded at first', function() {
          catsCtrl = createCatsController();
    
          expect(catsCtrl.cats).toBeDefined();
          expect(catsCtrl.cats.length).toEqual(0);
        });
    
        it('should call "activate()" on load, but only once', function() {
          catsCtrl = createCatsController();
          spyOn(catsCtrl, 'activate').and.returnValue(mockCatsData);
    
          // *** For some reason, Auto-Executing init functions
          // aren't working for me in Plunkr?
          // I have to call it once manually instead of relying on
          // $scope creation to do it... Sorry, not sure why.
          catsCtrl.activate();
          $rootScope.$digest();   // ELSE ...then() does NOT resolve.
    
          expect(catsCtrl.activate).toBeDefined();
          expect(catsCtrl.activate).toHaveBeenCalled();
          expect(catsCtrl.activate.calls.count()).toEqual(1);
    
          // Test/Expect additional  conditions for 
            // "Yes, the controller was activated right!"
          // (A) - there is be cats
          expect(catsCtrl.cats.length).toBeGreaterThan(0);
        });
    
        // (B) - there is be cats SUCH THAT
          // can haz these properties...
        it('each cat will have a NAME, TITLE and SCORE', function() {
          catsCtrl = createCatsController();
          spyOn(catsCtrl, 'activate').and.returnValue(mockCatsData);
    
          // *** and again...
          catsCtrl.activate();
          $rootScope.$digest();   // ELSE ...then() does NOT resolve.
    
          var names = _.map(catsCtrl.cats, function(cat) { return cat.name; })
          var titles = _.map(catsCtrl.cats, function(cat) { return cat.title; })
          var scores = _.map(catsCtrl.cats, function(cat) { return cat.score; })
    
          expect(names.length).toEqual(3);
          expect(titles.length).toEqual(3);
          expect(scores.length).toEqual(3); 
        });
    
    });
    

    规范:猫服务

    'use strict';
    
    describe('Unit Tests: Cats Service', function() {
    
      var $scope, $rootScope, $log, cats, logging, $httpBackend, mockCatsData;
    
      beforeEach(module('catsApp'));
      beforeEach(module('catsApp.mocks'));
    
      describe('has a method: getAllCats() that', function() {
    
        beforeEach(inject(function($q, _$rootScope_, _$httpBackend_, _$log_, $injector, StaticCatsData) {
          cats = $injector.get('CatsService');
          $rootScope = _$rootScope_;
          $httpBackend = _$httpBackend_;
    
          // We don't want to test the resolving of *actual data*
          // in a unit test.
          // The "proper" place for that is in Integration Test, which
          // is basically a unit test that is less mocked - you test
          // the endpoints and responses and APIs instead of the
          // specific service behaviors.
          mockCatsData = StaticCatsData();
    
          // For handling Promises and deferrals in our Service calls...
          var deferred = $q.defer();
          deferred.resolve(mockCatsData); //  always resolved, you can do it from your spec
    
          // jasmine 2.0
            // Spy + Promise Mocking
            // spyOn(obj, 'method'), (assumes obj.method is a function)
          spyOn(cats, 'getAllCats').and.returnValue(deferred.promise);
    
          /*
            To mock $http as a dependency, use $httpBackend to
            setup HTTP calls and expectations.
          */
          $httpBackend.whenGET('/cats').respond(200, mockCatsData);
        }));
    
        afterEach(function() {
          $httpBackend.verifyNoOutstandingExpectation();
          $httpBackend.verifyNoOutstandingRequest();
        })
    
        it(' exists/is defined', function() {
          expect( cats.getAllCats ).toBeDefined();
          expect( typeof cats.getAllCats ).toEqual("function");
        });
    
        it(' returns an array of Cats, where each cat has a NAME, TITLE and SCORE', function() {
          cats.getAllCats().then(function(data) {
            var names = _.map(data, function(cat) { return cat.name; })
            var titles = _.map(data, function(cat) { return cat.title; })
            var scores = _.map(data, function(cat) { return cat.score; })
    
            expect(names.length).toEqual(3);
            expect(titles.length).toEqual(3);
            expect(scores.length).toEqual(3);
          })
        });
    
      })
    
      describe('has a method: getAllCats() that also logs', function() {
    
          var cats, $log, logging;
    
          beforeEach(inject(
            function(_$log_, $injector) {
              cats = $injector.get('CatsService');
              $log = _$log_;
              logging = $injector.get('LoggingService');
    
              spyOn(cats, 'getAllCats').and.callThrough();
            }
          ))
    
          it('that on SUCCESS, $logs to the console a success message', function() {
            cats.getAllCats().then(function(data) {
              expect(logging.write).toHaveBeenCalled();
              expect( $log.log.logs ).toContain(["CatsService::getAllCats()::Success!"]);
            })
          });
    
        })
    
    });
    

    编辑 基于一些 cmets,我将我的答案更新为稍微复杂一些,并且我还编写了一个 Plunkr 演示单元测试。 具体来说,其中一个 cmets 提到“如果控制器的服务本身有一个简单的依赖项,例如 $log,该怎么办?” - 包含在测试用例的示例中。 希望能帮助到你!测试或破解地球!!!

    https://embed.plnkr.co/aSPHnr/

    【讨论】:

      【解决方案3】:

      我需要测试一个需要另一个指令 Google Places Autocomplete 的指令,我正在考虑是否应该模拟它...无论如何,这不会为需要 gPlacesAutocomplete 的指令抛出任何错误。

      describe('Test directives:', function() {
          beforeEach(module(...));
          beforeEach(module(...));
          beforeEach(function() {
              angular.module('google.places', [])
              .directive('gPlacesAutocomplete',function() {
                  return {
                      require: ['ngModel'],
                      restrict: 'A',
                      scope:{},
                      controller: function() { return {}; }
                   };
              });
           });
           beforeEach(module('google.places'));
      });
      

      【讨论】:

        【解决方案4】:

        如果你想测试一个控制器,你可以像下面这样注入和测试它。

        describe('When access Controller', function () {
            beforeEach(module('app'));
        
            var $controller;
        
            beforeEach(inject(function (_$controller_) {
                // The injector unwraps the underscores (_) from around the parameter names when matching
                $controller = _$controller_;
            }));
        
            describe('$scope.objectState', function () {
                it('is saying hello', function () {
                    var $scope = {};
                    var controller = $controller('yourController', { $scope: $scope });
                    expect($scope.objectState).toEqual('hello');
                });
            });
        });
        

        【讨论】:

        • 问题是关于测试服务,而不是控制器。
        猜你喜欢
        • 2014-06-17
        • 2014-09-04
        • 1970-01-01
        • 1970-01-01
        • 2013-03-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多