การใส่ล้อเลียนลงในบริการ AngularJS


114

ฉันมีบริการ AngularJS ที่เขียนไว้และฉันต้องการทดสอบหน่วย

angular.module('myServiceProvider', ['fooServiceProvider', 'barServiceProvider']).
    factory('myService', function ($http, fooService, barService) {

    this.something = function() {
        // Do something with the injected services
    };

    return this;
});

ไฟล์ app.js ของฉันได้ลงทะเบียนสิ่งเหล่านี้:

angular
.module('myApp', ['fooServiceProvider','barServiceProvider','myServiceProvider']
)

ฉันสามารถทดสอบว่า DI ทำงานได้ดังนี้:

describe("Using the DI framework", function() {
    beforeEach(module('fooServiceProvider'));
    beforeEach(module('barServiceProvider'));
    beforeEach(module('myServiceProvder'));

    var service;

    beforeEach(inject(function(fooService, barService, myService) {
        service=myService;
    }));

    it("can be instantiated", function() {
        expect(service).not.toBeNull();
    });
});

สิ่งนี้พิสูจน์แล้วว่าบริการสามารถสร้างขึ้นได้โดยเฟรมเวิร์ก DI อย่างไรก็ตามต่อไปฉันต้องการทดสอบหน่วยบริการซึ่งหมายถึงการเยาะเย้ยวัตถุที่ฉีดเข้าไป

ฉันจะทำสิ่งนี้ได้อย่างไร?

ฉันได้ลองใส่วัตถุจำลองในโมดูลแล้วเช่น

beforeEach(module(mockNavigationService));

และเขียนนิยามบริการใหม่เป็น:

function MyService(http, fooService, barService) {
    this.somthing = function() {
        // Do something with the injected services
    };
});

angular.module('myServiceProvider', ['fooServiceProvider', 'barServiceProvider']).
    factory('myService', function ($http, fooService, barService) { return new MyService($http, fooService, barService); })

แต่ช่วงหลังดูเหมือนว่า DI จะหยุดบริการที่สร้างขึ้นโดย DI ทั้งหมด

มีใครรู้บ้างไหมว่าฉันจะล้อเลียนบริการฉีดสำหรับการทดสอบหน่วยของฉันได้อย่างไร

ขอบคุณ

เดวิด


คุณสามารถดูที่นี้คำตอบของฉันคำถามอื่นผมหวังว่ามันจะเป็นประโยชน์กับคุณ
remigio

คำตอบ:


183

คุณสามารถฉีด mocks $provideในการให้บริการของคุณโดยใช้

หากคุณมีบริการต่อไปนี้ที่มีการพึ่งพาซึ่งมีเมธอดที่เรียกว่า getSomething:

angular.module('myModule', [])
  .factory('myService', function (myDependency) {
        return {
            useDependency: function () {
                return myDependency.getSomething();
            }
        };
  });

คุณสามารถฉีด myDependency เวอร์ชันจำลองได้ดังนี้:

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

  var mockDependency;

  beforeEach(module('myModule'));

  beforeEach(function () {

      mockDependency = {
          getSomething: function () {
              return 'mockReturnValue';
          }
      };

      module(function ($provide) {
          $provide.value('myDependency', mockDependency);
      });

  });

  it('should return value from mock dependency', inject(function (myService) {
      expect(myService.useDependency()).toBe('mockReturnValue');
  }));

});

โปรดทราบว่าเนื่องจากการโทรหา$provide.valueคุณไม่จำเป็นต้องฉีด myDependency อย่างชัดเจนที่ใดก็ได้ มันเกิดขึ้นภายใต้ประทุนระหว่างการฉีด myService เมื่อตั้งค่า mockDependency ที่นี่อาจเป็นสายลับได้อย่างง่ายดาย

ขอบคุณภักดีบราวน์สำหรับลิงก์ไปยังวิดีโอที่ยอดเยี่ยมนั้น


13
ใช้งานได้ดี แต่ระวังรายละเอียดที่: beforeEach(module('myModule'));โทรHASจะมาก่อนที่จะbeforeEach(function () { MOCKING })โทรหรืออื่น mocks จะได้รับการเขียนทับโดยการให้บริการจริง!
Nikos Paraskevopoulos

1
มีวิธีการล้อเลียนไม่ให้บริการ แต่คงที่ในลักษณะเดียวกันหรือไม่?
Artem

5
คล้ายกับความคิดเห็นของ Nikos $provideต้องโทรออกก่อนใช้งาน$injectorมิฉะนั้นคุณจะได้รับข้อผิดพลาด:Injector already created, can not register a module!
providencemac

7
จะเกิดอะไรขึ้นถ้าการจำลองของคุณต้องการ $ q? จากนั้นคุณไม่สามารถฉีด $ q ลงในการจำลองก่อนที่จะเรียกโมดูล () เพื่อลงทะเบียนการจำลอง ความคิดใด ๆ ?
Jake

4
หากคุณกำลังใช้ CoffeeScript และคุณเห็นกำลังError: [ng:areq] Argument 'fn' is not a function, got Objectให้แน่ใจว่าจะใส่บนเส้นหลังreturn $provide.value(...)การส่งคืนโดยปริยาย$provide.value(...)ทำให้เกิดข้อผิดพลาดนั้นสำหรับฉัน
yndolok

4

วิธีที่ฉันดูไม่จำเป็นต้องล้อเลียนบริการตัวเอง เพียงแค่ล้อเลียนฟังก์ชั่นในบริการ ด้วยวิธีนี้คุณสามารถฉีดเชิงมุมบริการจริงของคุณได้เหมือนเดิมตลอดทั้งแอป จากนั้นจำลองฟังก์ชันในบริการตามต้องการโดยใช้spyOnฟังก์ชันของจัสมิน

ตอนนี้ถ้าบริการนั้นเป็นฟังก์ชันไม่ใช่วัตถุที่คุณสามารถใช้ได้spyOnมีวิธีอื่นในการดำเนินการดังกล่าว ฉันต้องทำสิ่งนี้และพบว่ามีบางอย่างที่ใช้ได้ผลกับฉัน ดูว่าคุณล้อเลียนบริการ Angular ที่เป็นฟังก์ชันได้อย่างไร?


3
ฉันไม่คิดว่าสิ่งนี้จะตอบคำถามได้ จะเกิดอะไรขึ้นถ้าโรงงานของบริการที่ถูกล้อเลียนทำสิ่งที่ไม่สำคัญเช่นตีเซิร์ฟเวอร์เพื่อหาข้อมูล? นั่นคงเป็นเหตุผลที่ดีที่อยากจะล้อเลียนมัน คุณต้องการหลีกเลี่ยงการเรียกเซิร์ฟเวอร์และสร้างบริการจำลองที่มีข้อมูลปลอมแทน การล้อเลียน $ http ไม่ใช่วิธีแก้ปัญหาที่ดีเช่นกันเพราะคุณกำลังทดสอบบริการสองบริการในการทดสอบเดียวแทนที่จะทดสอบหน่วยบริการทั้งสองแยกกัน ดังนั้นฉันจึงขอย้ำคำถามอีกครั้ง คุณจะส่งบริการจำลองไปยังบริการอื่นในการทดสอบหน่วยอย่างไร?
Patrick Arnesen

1
หากคุณกังวลเกี่ยวกับบริการที่กดเซิร์ฟเวอร์เพื่อขอข้อมูลนั่นคือสิ่งที่ $ httpBackend มีไว้สำหรับ ( docs.angularjs.org/api/ngMock.$httpBackend ) ฉันไม่แน่ใจว่าจะมีข้อกังวลอะไรอีกในโรงงานบริการที่ต้องล้อเลียนบริการทั้งหมด
dnc253

2

อีกทางเลือกหนึ่งที่จะช่วยให้การจำลองการอ้างอิงง่ายขึ้นใน Angular และ Jasmine คือการใช้ QuickMock สามารถพบได้ใน GitHub และช่วยให้คุณสร้างล้อเลียนง่ายๆด้วยวิธีที่ใช้ซ้ำได้ คุณสามารถโคลนได้จาก GitHub ผ่านลิงค์ด้านล่าง README ค่อนข้างอธิบายตัวเองได้ แต่หวังว่ามันอาจช่วยคนอื่นได้ในอนาคต

https://github.com/tennisgent/QuickMock

describe('NotificationService', function () {
    var notificationService;

    beforeEach(function(){
        notificationService = QuickMock({
            providerName: 'NotificationService', // the provider we wish to test
            moduleName: 'QuickMockDemo',         // the module that contains our provider
            mockModules: ['QuickMockDemoMocks']  // module(s) that contains mocks for our provider's dependencies
        });
    });
    ....

มันจะจัดการแผ่นสำเร็จรูปทั้งหมดที่กล่าวถึงข้างต้นโดยอัตโนมัติดังนั้นคุณไม่จำเป็นต้องเขียนรหัสการฉีดจำลองทั้งหมดในการทดสอบทุกครั้ง หวังว่าจะช่วยได้


2

นอกเหนือจากคำตอบของ John Galambos : หากคุณต้องการล้อเลียนวิธีการบริการเฉพาะคุณสามารถทำได้ดังนี้:

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

  var mockDependency;

  beforeEach(module('myModule'));

  beforeEach(module(function ($provide, myDependencyProvider) {
      // Get an instance of the real service, then modify specific functions
      mockDependency = myDependencyProvider.$get();
      mockDependency.getSomething = function() { return 'mockReturnValue'; };
      $provide.value('myDependency', mockDependency);
  });

  it('should return value from mock dependency', inject(function (myService) {
      expect(myService.useDependency()).toBe('mockReturnValue');
  }));

});

1

หากคอนโทรลเลอร์ของคุณถูกเขียนให้รับการพึ่งพาเช่นนี้:

app.controller("SomeController", ["$scope", "someDependency", function ($scope, someDependency) {
    someDependency.someFunction();
}]);

จากนั้นคุณสามารถทำของปลอมsomeDependencyในการทดสอบจัสมินเช่นนี้:

describe("Some Controller", function () {

    beforeEach(module("app"));


    it("should call someMethod on someDependency", inject(function ($rootScope, $controller) {
        // make a fake SomeDependency object
        var someDependency = {
            someFunction: function () { }
        };

        spyOn(someDependency, "someFunction");

        // this instantiates SomeController, using the passed in object to resolve dependencies
        controller("SomeController", { $scope: scope, someDependency: someDependency });

        expect(someDependency.someFunction).toHaveBeenCalled();
    }));
});

9
คำถามคือเกี่ยวกับบริการซึ่งไม่ได้รับการสร้างอินสแตนซ์ในชุดทดสอบด้วยการโทรไปยังบริการที่เทียบเท่าในฐานะ $ controller กล่าวอีกนัยหนึ่งไม่มีใครเรียก $ service () ใน beforeEach block โดยส่งผ่านการอ้างอิง
Morris Singer

1

ฉันเพิ่งเปิดตัว ngImprovedTesting ซึ่งน่าจะทำให้การทดสอบจำลองใน AngularJS ง่ายขึ้น

ในการทดสอบ 'myService' (จากโมดูล "myApp") ด้วยการอ้างอิง fooService และ barService ที่เยาะเย้ยคุณสามารถทำสิ่งต่อไปนี้ในการทดสอบ Jasmine ของคุณ:

beforeEach(ModuleBuilder
    .forModule('myApp')
    .serviceWithMocksFor('myService', 'fooService', 'barService')
    .build());

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับ ngImprovedTesting โปรดดูบล็อกโพสต์เบื้องต้น: http://blog.jdriven.com/2014/07/ng-improved-testing-mock-testing-for-angularjs-made-easy/


1
เหตุใดจึงได้รับการโหวตลดลง ฉันไม่เข้าใจคุณค่าของการลงคะแนนโดยไม่มีความคิดเห็น
Jacob Brewer

0

ฉันรู้ว่านี่เป็นคำถามเก่า แต่มีอีกวิธีที่ง่ายกว่าคือคุณสามารถสร้างจำลองและปิดการใช้งานการฉีดต้นฉบับในฟังก์ชันเดียวซึ่งสามารถทำได้โดยใช้ spyOn ในทุกวิธี ดูรหัสด้านล่าง

var mockInjectedProvider;

    beforeEach(function () {
        module('myModule');
    });

    beforeEach(inject(function (_injected_) { 
      mockInjectedProvider  = mock(_injected_);
    });

    beforeEach(inject(function (_base_) {
        baseProvider = _base_;
    }));

    it("injectedProvider should be mocked", function () {
    mockInjectedProvider.myFunc.andReturn('testvalue');    
    var resultFromMockedProvider = baseProvider.executeMyFuncFromInjected();
        expect(resultFromMockedProvider).toEqual('testvalue');
    }); 

    //mock all service methods
    function mock(angularServiceToMock) {

     for (var i = 0; i < Object.getOwnPropertyNames(angularServiceToMock).length; i++) {
      spyOn(angularServiceToMock,Object.getOwnPropertyNames(angularServiceToMock)[i]);
     }
                return angularServiceToMock;
    }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.