ฉันจะทดสอบบริการ AngularJS กับ Jasmine ได้อย่างไร


108

(มีคำถามที่เกี่ยวข้องที่นี่: การทดสอบจัสมินไม่เห็นโมดูล AngularJS )

ฉันแค่ต้องการทดสอบบริการโดยไม่ต้อง bootstrapping Angular

ฉันได้ดูตัวอย่างและบทแนะนำแล้ว แต่ฉันไม่ได้ไปไหน

ฉันมีเพียงสามไฟล์:

  • myService.js: ที่ฉันกำหนดบริการ AngularJS

  • test_myService.js: ที่ฉันกำหนดการทดสอบจัสมินสำหรับบริการ

  • specRunner.html: ไฟล์ 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);
        })

    })

});

คำตอบ:


137

ปัญหาคือวิธีการของโรงงานที่สร้างอินสแตนซ์ของบริการไม่ถูกเรียกในตัวอย่างด้านบน (เฉพาะการสร้างโมดูลเท่านั้นที่ไม่ได้สร้างอินสแตนซ์ของบริการ)

เพื่อให้บริการถูกสร้างอินสแตนซ์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);
        })

    })

});

อีกวิธีหนึ่งคือการส่งผ่านบริการไปยังฟังก์ชันโดยใช้ " เรียกใช้ ":

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 ' เมื่อทำเช่นนี้เราต้องตระหนักว่าฟังก์ชัน 'ฉีด' มันไม่ได้อยู่ในแพ็คเกจ angularjs มาตรฐาน แต่อยู่ในโมดูล ngMock และใช้ได้กับจัสมินเท่านั้น

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
ชอบที่จะเห็นตัวอย่างเมื่อบริการของคุณมีการพึ่งพาของตนเอง (เช่น $ log)
Roy Truelove

2
ขออภัยฉันกำลังมองหาสิ่งนี้จริงๆ: stackoverflow.com/q/16565531/295797
Roy Truelove

1
มีวิธีที่ดีในการฉีดบริการในbeforeEachกรณีที่มีหลายคน ... หลายคน ... การทดสอบหลายอย่างที่จำเป็นสำหรับการบริการหรือไม่? การทดสอบโมเดลข้อมูล (บริการ) และมีตัวแปรทั่วโลกจำนวนมาก ขอบคุณC§
CSS

2
คุณไม่ได้บอกว่าทำไม (3) ถึงเป็น 'วิธีที่เหมาะสม'
LeeGee

2
@LeeGee ฉันคิดว่าเราสามารถเรียกมันว่า 'ถูกต้อง' ได้เพราะมันใช้โมดูล ngMock AngularJS ที่มีไว้เพื่อการทดสอบโดยเฉพาะ
Robert

5

แม้ว่าคำตอบข้างต้นอาจใช้งานได้ดี (ฉันยังไม่ได้ลอง :)) ฉันมักจะมีการทดสอบอีกมากมายที่จะเรียกใช้ดังนั้นฉันจึงไม่ได้ทำการทดสอบด้วยตัวเอง ฉันจะจัดกลุ่ม () กรณีเป็นอธิบายบล็อกและเรียกใช้การแทรกของฉันใน beforeEach () หรือ beforeAll () ในแต่ละบล็อกอธิบาย

โรเบิร์ตก็ถูกต้องเช่นกันที่เขาบอกว่าคุณต้องใช้ Angular $ injector เพื่อให้การทดสอบทราบถึงบริการหรือโรงงาน Angular ใช้หัวฉีดนี้ในแอปพลิเคชันของคุณด้วยเพื่อบอกแอปพลิเคชันว่ามีอะไรบ้าง อย่างไรก็ตามสามารถเรียกได้มากกว่าหนึ่งแห่งและยังสามารถเรียกโดยปริยายแทนอย่างชัดเจน คุณจะสังเกตเห็นในไฟล์ทดสอบข้อมูลจำเพาะตัวอย่างของฉันด้านล่างบล็อก beforeEach () เรียกโดยปริยายว่า injectorเพื่อให้สิ่งต่างๆพร้อมที่จะกำหนดภายในการทดสอบ

กลับไปที่การจัดกลุ่มสิ่งต่างๆและการใช้ก่อนบล็อกต่อไปนี้เป็นตัวอย่างเล็ก ๆ ฉันกำลังทำ Cat Service และต้องการทดสอบดังนั้นการตั้งค่าง่ายๆในการเขียนและทดสอบบริการจะมีลักษณะดังนี้:

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

ข้อมูลจำเพาะ: Cats Controller

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

    })

});

แก้ไข จากความคิดเห็นบางส่วนฉันได้อัปเดตคำตอบให้ซับซ้อนขึ้นเล็กน้อยและฉันได้สร้าง Plunkr ที่สาธิตการทดสอบหน่วยด้วย โดยเฉพาะอย่างยิ่งความคิดเห็นหนึ่งกล่าวถึง "จะเกิดอะไรขึ้นถ้าบริการของผู้ควบคุมมีการพึ่งพาอย่างง่ายเช่น $ log?" - ซึ่งรวมอยู่ในตัวอย่างพร้อมกรณีทดสอบ หวังว่าจะช่วยได้! ทดสอบหรือสับโลก !!!

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


0

ฉันต้องการทดสอบคำสั่งที่ต้องใช้คำสั่งอื่นการเติมข้อความอัตโนมัติของ Google สถานที่ฉันกำลังชั่งใจว่าฉันควรจะล้อเลียนหรือไม่ ... อย่างไรก็ตามวิธีนี้ใช้ได้ผลโดยไม่ทิ้งข้อผิดพลาดใด ๆ สำหรับคำสั่งที่ต้องใช้ 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

หากคุณต้องการทดสอบคอนโทรลเลอร์คุณสามารถฉีดและทดสอบได้ตามด้านล่าง

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
คำถามเกี่ยวกับบริการทดสอบไม่ใช่ตัวควบคุม
Bartek S
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.