Bagaimana cara menguji layanan AngularJS dengan Jasmine?


107

(Ada pertanyaan terkait di sini: Tes Jasmine tidak melihat modul AngularJS )

Saya hanya ingin menguji layanan tanpa bootstrap Angular.

Saya telah melihat beberapa contoh dan tutorial tetapi saya tidak ke mana-mana.

Saya hanya memiliki tiga file:

  • myService.js: tempat saya mendefinisikan layanan AngularJS

  • test_myService.js: di mana saya mendefinisikan pengujian Jasmine untuk layanan tersebut.

  • specRunner.html: file HTML dengan konfigurasi jasmine normal dan tempat saya mengimpor dua file sebelumnya dan Jasmine, Angularjs, dan angular-mocks.js.

Ini adalah kode untuk layanan (yang berfungsi seperti yang diharapkan saat saya tidak menguji):

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

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

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

    return serviceImplementation

});

Saat saya mencoba menguji layanan secara terpisah, saya seharusnya dapat mengaksesnya dan memeriksa metode mereka. Pertanyaan saya adalah: bagaimana saya dapat menyuntikkan layanan dalam pengujian saya tanpa bootstrap AngularJS?

Misalnya, bagaimana saya bisa menguji nilai yang dikembalikan untuk metode layanan dengan Jasmine seperti ini:

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

    })

});

Jawaban:


137

Masalahnya adalah bahwa metode pabrik, yang membuat layanan, tidak dipanggil dalam contoh di atas (hanya membuat modul tidak membuat layanan tersebut).

Untuk layanan yang akan dipakai angular.injector harus dipanggil dengan modul tempat layanan kami didefinisikan. Kemudian, kita dapat meminta objek injektor baru untuk layanan tersebut dan hanya setelah layanan tersebut dibuatkan instance-nya.

Sesuatu seperti ini berfungsi:

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

    })

});

Cara lain adalah meneruskan layanan ke fungsi menggunakan ' 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 );
        })

    })

});

Dan, terakhir, cara yang 'tepat' untuk melakukannya adalah menggunakan ' inject ' dan ' module ' di blok jasmine ' beforeEach '. Saat melakukannya kita harus menyadari bahwa fungsi 'inject' tidak ada dalam paket standar angularjs, tetapi dalam modul ngMock dan hanya bekerja dengan 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
Ingin sekali melihat contoh ketika layanan Anda memiliki dependensinya sendiri (mis. $ Log)
Roy Truelove

2
Maaf, saya sebenarnya sedang mencari sesuatu seperti ini: stackoverflow.com/q/16565531/295797
Roy Truelove

1
Adakah cara yang baik untuk memasukkan layanan dalam beforeEachkasus banyak ... banyak ... banyak tes yang diperlukan untuk layanan? Menguji model data (layanan) dan memegang banyak variabel global. Terima kasih, C§
CSS

2
Anda tidak mengatakan mengapa (3) adalah 'cara yang tepat'
LeeGee

2
@LeeGee Saya pikir kita bisa menyebutnya cara yang 'tepat' karena menggunakan modul ngMock AngularJS yang ada di sana khusus untuk tujuan pengujian.
Robert

5

Sementara jawaban di atas mungkin berfungsi dengan baik (saya belum mencobanya :)), saya sering memiliki lebih banyak tes untuk dijalankan sehingga saya tidak menyuntikkan tes itu sendiri. Saya akan mengelompokkan kasus it () menjadi mendeskripsikan blok dan menjalankan injeksi saya di beforeEach () atau beforeAll () di setiap blok mendeskripsikan.

Robert juga benar bahwa dia mengatakan Anda harus menggunakan injektor $ Angular untuk membuat tes mengetahui layanan atau pabrik. Angular menggunakan injektor ini sendiri dalam aplikasi Anda, juga untuk memberi tahu aplikasi apa yang tersedia. Namun, ini bisa dipanggil di lebih dari satu tempat, dan juga bisa dipanggil secara implisit, bukan eksplisit. Anda akan melihat dalam contoh file pengujian spesifikasi saya di bawah ini, blok beforeEach () secara implisit memanggil injektor untuk membuat hal-hal tersedia untuk ditugaskan di dalam pengujian.

Kembali ke pengelompokan hal-hal dan menggunakan blok sebelumnya, berikut adalah contoh kecilnya. Saya membuat Cat Service dan saya ingin mengujinya, jadi pengaturan sederhana saya untuk menulis dan menguji Service akan terlihat seperti ini:

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

Spesifikasi: Pengontrol Kucing

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

});

Spesifikasi: Layanan Kucing

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

    })

});

EDIT Berdasarkan beberapa komentar, saya telah memperbarui jawaban saya menjadi sedikit lebih kompleks, dan saya juga membuat Plunkr yang mendemonstrasikan Unit Testing. Secara khusus, salah satu komentar menyebutkan "Bagaimana jika Layanan Pengontrol memiliki ketergantungan sederhana, seperti $ log?" - yang termasuk dalam contoh dengan kasus uji. Semoga membantu! Uji atau Retas Planet !!!

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


0

Saya perlu menguji arahan yang membutuhkan arahan lain, Google Places Autocomplete , saya berdebat apakah saya harus mengejeknya ... bagaimanapun ini berhasil tanpa membuang kesalahan untuk arahan yang membutuhkan 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'));
});

-5

Jika Anda ingin menguji pengontrol, Anda dapat menyuntikkan dan mengujinya seperti di bawah ini.

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
Pertanyaannya adalah tentang layanan pengujian, bukan pengontrol.
Bartek S
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.