Làm cách nào để kiểm tra dịch vụ AngularJS với Jasmine?


107

(Có một câu hỏi liên quan ở đây: Thử nghiệm Jasmine không thấy mô-đun AngularJS )

Tôi chỉ muốn thử nghiệm một dịch vụ mà không cần khởi động Angular.

Tôi đã xem một số ví dụ và hướng dẫn nhưng tôi sẽ không đi đâu cả.

Tôi chỉ có ba tệp:

  • myService.js: nơi tôi xác định dịch vụ AngularJS

  • test_myService.js: nơi tôi xác định thử nghiệm Jasmine cho dịch vụ.

  • specRunner.html: một tệp HTML với cấu hình jasmine bình thường và nơi tôi nhập hai tệp khác trước đó và Jasmine, Angularjs và angle-mocks.js.

Đây là mã cho dịch vụ (hoạt động như mong đợi khi tôi không thử nghiệm):

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

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

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

    return serviceImplementation

});

Vì tôi đang cố gắng kiểm tra dịch vụ một cách riêng biệt, tôi có thể truy cập nó và kiểm tra các phương pháp của họ. Câu hỏi của tôi là: làm cách nào tôi có thể đưa dịch vụ vào thử nghiệm của mình mà không cần khởi động AngularJS?

Ví dụ: làm cách nào để tôi có thể kiểm tra giá trị được trả về cho một phương thức của dịch vụ với Jasmine như sau:

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);
        })

    })

});

Câu trả lời:


137

Vấn đề là phương thức gốc, khởi tạo dịch vụ, không được gọi trong ví dụ trên (chỉ tạo mô-đun không khởi tạo dịch vụ).

Để dịch vụ được khởi tạo angle.injector phải được gọi với mô-đun nơi dịch vụ của chúng ta được xác định. Sau đó, chúng ta có thể yêu cầu đối tượng đầu phun mới cho dịch vụ và chỉ sau đó khi dịch vụ cuối cùng được khởi tạo.

Một cái gì đó như thế này hoạt động:

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);
        })

    })

});

Một cách khác sẽ là chuyển dịch vụ đến một hàm bằng cách sử dụng ' gọi ':

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 );
        })

    })

});

Và, cuối cùng, cách 'thích hợp' để làm điều đó là sử dụng ' tiêm ' và ' mô-đun ' trong một khối hoa nhài ' beforeEach '. Khi thực hiện, chúng ta phải nhận ra rằng hàm 'tiêm' nó không nằm trong gói anglejs tiêu chuẩn, mà nằm trong mô-đun ngMock và nó chỉ hoạt động với 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);

        }))

    })

});

13
Rất thích xem ví dụ về thời điểm dịch vụ của bạn có các phụ thuộc của riêng nó (ví dụ: $ log)
Roy Truelove

2
Xin lỗi, tôi thực sự đang tìm kiếm một cái gì đó như thế này: stackoverflow.com/q/16565531/295797
Roy Truelove

1
Có cách nào tốt để tiêm dịch vụ trong beforeEachtrường hợp có nhiều ... nhiều ... nhiều xét nghiệm cần thiết cho dịch vụ không? Thử nghiệm một mô hình dữ liệu (dịch vụ) và nó chứa rất nhiều biến toàn cục. Cảm ơn, C§
CSS

2
Bạn không nói lý do tại sao (3) là 'cách thích hợp'
LeeGee

2
@LeeGee Tôi nghĩ chúng ta có thể gọi nó theo cách 'thích hợp' vì nó sử dụng mô-đun ngMock AngularJS mà nó ở đó đặc biệt cho mục đích thử nghiệm.
Robert

5

Mặc dù câu trả lời ở trên có lẽ hoạt động tốt (tôi chưa thử :))), tôi thường phải chạy nhiều thử nghiệm hơn nên tôi không tự đưa vào thử nghiệm. Tôi sẽ nhóm các trường hợp it () thành các khối mô tả và chạy tiêm của tôi trong một beforeEach () hoặc beforeAll () trong mỗi khối mô tả.

Robert cũng đúng ở chỗ anh ấy nói rằng bạn phải sử dụng bộ tiêm Angular $ để làm cho các bài kiểm tra biết dịch vụ hoặc nhà máy. Angular cũng sử dụng chính bộ tiêm này trong các ứng dụng của bạn để cho ứng dụng biết những gì có sẵn. Tuy nhiên, nó có thể được gọi ở nhiều nơi, và nó cũng có thể được gọi ngầm thay vì rõ ràng. Bạn sẽ nhận thấy trong tệp kiểm tra thông số kỹ thuật ví dụ của tôi bên dưới, khối beforeEach () ngầm gọi bộ tiêm để cung cấp những thứ có sẵn để được gán bên trong các bài kiểm tra.

Quay trở lại với việc nhóm mọi thứ và sử dụng khối trước, đây là một ví dụ nhỏ. Tôi đang tạo Dịch vụ dành cho mèo và tôi muốn thử nghiệm nó, vì vậy thiết lập đơn giản của tôi để viết và kiểm tra Dịch vụ sẽ trông như sau:

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);

Spec: Bộ điều khiển mèo

'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); 
    });

});

Spec: Dịch vụ mèo

'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!"]);
        })
      });

    })

});

CHỈNH SỬA Dựa trên một số nhận xét, tôi đã cập nhật câu trả lời của mình để phức tạp hơn một chút và tôi cũng đã tạo ra một Bài kiểm tra đơn vị trình diễn Plunkr. Cụ thể, một trong những bình luận đã đề cập đến "Điều gì sẽ xảy ra nếu Dịch vụ của bộ điều khiển có một phụ thuộc đơn giản, chẳng hạn như $ log?" - được bao gồm trong ví dụ với các trường hợp thử nghiệm. Hy vọng nó giúp! Kiểm tra hoặc Hack hành tinh !!!

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


0

Tôi cần kiểm tra một chỉ thị yêu cầu một chỉ thị khác, Tự động điền của Google Địa điểm , tôi đang tranh luận về việc liệu tôi có nên mô phỏng nó hay không ... dù sao thì điều này đã hoạt động mà không bỏ qua bất kỳ lỗi nào cho chỉ thị yêu cầu gPordsAutocomplete.

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'));
});

-5

Nếu bạn muốn kiểm tra một bộ điều khiển, bạn có thể tiêm và kiểm tra nó như bên dưới.

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');
        });
    });
});

2
câu hỏi là về dịch vụ thử nghiệm, không phải bộ điều khiển.
Bartek S
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.