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


152

ฉันมีmyServiceสิ่งที่ใช้myOtherServiceซึ่งทำให้การโทรระยะไกลคืนสัญญา:

angular.module('app.myService', ['app.myOtherService'])
  .factory('myService', [
    myOtherService,
    function(myOtherService) {
      function makeRemoteCall() {
        return myOtherService.makeRemoteCallReturningPromise();
      }

      return {
        makeRemoteCall: makeRemoteCall
      };      
    }
  ])

ในการสร้างการทดสอบหน่วยสำหรับmyServiceฉันต้องเยาะเย้ยmyOtherServiceเช่นนั้นmakeRemoteCallReturningPromiseวิธีการของมันจะให้สัญญา นี่คือวิธีที่ฉันทำ:

describe('Testing remote call returning promise', function() {
  var myService;
  var myOtherServiceMock = {};

  beforeEach(module('app.myService'));

  // I have to inject mock when calling module(),
  // and module() should come before any inject()
  beforeEach(module(function ($provide) {
    $provide.value('myOtherService', myOtherServiceMock);
  }));

  // However, in order to properly construct my mock
  // I need $q, which can give me a promise
  beforeEach(inject(function(_myService_, $q){
    myService = _myService_;
    myOtherServiceMock = {
      makeRemoteCallReturningPromise: function() {
        var deferred = $q.defer();

        deferred.resolve('Remote call result');

        return deferred.promise;
      }    
    };
  }

  // Here the value of myOtherServiceMock is not
  // updated, and it is still {}
  it('can do remote call', inject(function() {
    myService.makeRemoteCall() // Error: makeRemoteCall() is not defined on {}
      .then(function() {
        console.log('Success');
      });    
  }));  

อย่างที่คุณเห็นจากข้างบนนิยามของคำเยาะเย้ยของฉันขึ้นอยู่กับ$qว่าฉันต้องโหลดโดยใช้inject()อะไร นอกจากนี้การฉีดจำลองที่ควรจะเกิดขึ้นในซึ่งควรจะมาก่อนmodule() inject()อย่างไรก็ตามค่าของการเยาะเย้ยนั้นไม่ได้รับการอัพเดตเมื่อฉันเปลี่ยนมัน

วิธีที่เหมาะสมในการทำเช่นนี้คืออะไร?


เกิดข้อผิดพลาดขึ้นจริงmyService.makeRemoteCall()หรือ ถ้าเป็นเช่นนั้นปัญหาอยู่ที่myServiceไม่ได้มีไม่ได้ทำอะไรกับเย้ยหยันของคุณmakeRemoteCall myOtherService
dnc253

ข้อผิดพลาดอยู่ใน myService.makeRemoteCall () เนื่องจาก myService.myOtherService เป็นเพียงวัตถุว่างเปล่า ณ จุดนี้ (ค่าของมันไม่เคยอัปเดตโดยเชิงมุม)
Georgii Oleinikov

คุณเพิ่มวัตถุว่างลงในคอนเทนเนอร์ ioc หลังจากนั้นคุณเปลี่ยนการอ้างอิง myOtherServiceMock ให้ชี้ไปที่วัตถุใหม่ที่คุณสอดแนม Whats ในคอนเทนเนอร์ ioc จะไม่สะท้อนสิ่งนั้นเนื่องจากการอ้างอิงนั้นเปลี่ยนไป
twDuke

คำตอบ:


175

ฉันไม่แน่ใจว่าทำไมวิธีที่คุณใช้งานไม่ได้ แต่ฉันมักจะทำกับspyOnฟังก์ชั่น บางสิ่งเช่นนี้

describe('Testing remote call returning promise', function() {
  var myService;

  beforeEach(module('app.myService'));

  beforeEach(inject( function(_myService_, myOtherService, $q){
    myService = _myService_;
    spyOn(myOtherService, "makeRemoteCallReturningPromise").and.callFake(function() {
        var deferred = $q.defer();
        deferred.resolve('Remote call result');
        return deferred.promise;
    });
  }

  it('can do remote call', inject(function() {
    myService.makeRemoteCall()
      .then(function() {
        console.log('Success');
      });    
  }));

โปรดจำไว้ว่าคุณจะต้อง$digestโทรออกเพื่อรับthenฟังก์ชั่นที่จะเรียกใช้ ดูการทดสอบในส่วนของเอกสาร $ Q

------ แก้ไข ------

หลังจากดูสิ่งที่คุณทำอย่างใกล้ชิดฉันคิดว่าฉันเห็นปัญหาในโค้ดของคุณ ในbeforeEach, คุณกำลังตั้งค่าmyOtherServiceMockเป็นวัตถุใหม่ทั้งหมด $provideจะไม่เคยเห็นเอกสารอ้างอิงนี้ คุณเพียงแค่ต้องอัปเดตข้อมูลอ้างอิงที่มีอยู่:

beforeEach(inject( function(_myService_, $q){
    myService = _myService_;
    myOtherServiceMock.makeRemoteCallReturningPromise = function() {
        var deferred = $q.defer();
        deferred.resolve('Remote call result');
        return deferred.promise;   
    };
  }

1
และคุณฆ่าฉันเมื่อวานนี้โดยไม่ปรากฏในผลลัพธ์ จอแสดงผลที่สวยงามของ andCallFake () ขอบคุณ.
Priya Ranjan Singh

แทนที่จะใช้andCallFakeคุณสามารถใช้andReturnValue(deferred.promise)(หรือand.returnValue(deferred.promise)ใน Jasmine 2.0+) คุณต้องกำหนดdeferredก่อนโทรspyOnแน่นอน
Jordan เริ่ม

1
คุณจะโทร$digestในกรณีนี้อย่างไรเมื่อคุณไม่มีสิทธิ์เข้าถึงขอบเขต
Jim Aho

7
@JimAho โดยปกติแล้วคุณเพียงแค่ฉีด$rootScopeและเรียก$digestมันว่า
dnc253

1
การใช้การเลื่อนเวลาในกรณีนี้ไม่จำเป็น คุณสามารถใช้$q.when() codelord.net/2015/09/24/$q-dot-defer-youre-doing-it-wrong
fodma1

69

นอกจากนี้เรายังสามารถเขียนการดำเนินการของจัสมินในการส่งคืนสัญญาโดยตรงโดยสายลับ

spyOn(myOtherService, "makeRemoteCallReturningPromise").andReturn($q.when({}));

สำหรับจัสมิน 2:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));

(คัดลอกมาจากความคิดเห็นขอบคุณ ccnokes)


12
หมายเหตุสำหรับผู้ใช้ Jasmine 2.0, .andReturn () ถูกแทนที่ด้วย. and.returnValue ตัวอย่างข้างต้นจะเป็น: spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));ฉันเพิ่งฆ่าครึ่งชั่วโมงคิดออกว่า
ccnokes

13
describe('testing a method() on a service', function () {    

    var mock, service

    function init(){
         return angular.mock.inject(function ($injector,, _serviceUnderTest_) {
                mock = $injector.get('service_that_is_being_mocked');;                    
                service = __serviceUnderTest_;
            });
    }

    beforeEach(module('yourApp'));
    beforeEach(init());

    it('that has a then', function () {
       //arrange                   
        var spy= spyOn(mock, 'actionBeingCalled').and.callFake(function () {
            return {
                then: function (callback) {
                    return callback({'foo' : "bar"});
                }
            };
        });

        //act                
        var result = service.actionUnderTest(); // does cleverness

        //assert 
        expect(spy).toHaveBeenCalled();  
    });
});

1
นี่คือสิ่งที่ฉันเคยทำในอดีต สร้างสายลับที่ส่งคืนของปลอมที่เลียนแบบ "แล้ว"
Darren Corbett

คุณช่วยยกตัวอย่างการทดสอบที่สมบูรณ์ที่คุณมีได้ไหม ฉันมีปัญหาคล้ายกันกับการมีบริการที่ส่งคืนสัญญา แต่ในนั้นยังทำให้การโทรที่ส่งกลับสัญญา!
Rob Paddock

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

ฉันเริ่มต้นเส้นทางนี้และใช้งานได้ดีกับสถานการณ์อย่างง่าย ฉันยังสร้างแบบจำลองที่จำลองการโยงและให้ตัวช่วย "หยุด" / "หยุด" เพื่อเรียกใช้โซ่gist.github.com/marknadig/c3e8f2d3fff9d22da42b ในสถานการณ์ที่ซับซ้อนกว่านี้ ในกรณีของฉันฉันมีบริการที่จะคืนสินค้าจากแคช (โดยไม่ต้องรอ) หรือทำการร้องขอ ดังนั้นมันกำลังสร้างสัญญาของตัวเอง
Mark Nadig

โพสต์นี้ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spyอธิบายการใช้งานของปลอม "จากนั้น" อย่างละเอียด
Custodio

8

คุณสามารถใช้ห้องสมุดการขัดถูเช่น sinon เพื่อเยาะเย้ยบริการของคุณ จากนั้นคุณสามารถส่งคืน $ q.when () เป็นสัญญาของคุณ หากค่าของขอบเขตวัตถุของคุณมาจากผลลัพธ์สัญญาคุณจะต้องเรียกขอบเขต $ root. $ digest ()

var scope, controller, datacontextMock, customer;
  beforeEach(function () {
        module('app');
        inject(function ($rootScope, $controller,common, datacontext) {
            scope = $rootScope.$new();
            var $q = common.$q;
            datacontextMock = sinon.stub(datacontext);
            customer = {id:1};
           datacontextMock.customer.returns($q.when(customer));

            controller = $controller('Index', { $scope: scope });

        })
    });


    it('customer id to be 1.', function () {


            scope.$root.$digest();
            expect(controller.customer.id).toBe(1);


    });

2
นี่คือชิ้นส่วนที่ขาดหายไปซึ่งเรียกว่า$rootScope.$digest()ได้รับคำสัญญาที่จะแก้ไข

2

ใช้sinon:

const mockAction = sinon.stub(MyService.prototype,'actionBeingCalled')
                     .returns(httpPromise(200));

รู้ว่าhttpPromiseสามารถ:

const httpPromise = (code) => new Promise((resolve, reject) =>
  (code >= 200 && code <= 299) ? resolve({ code }) : reject({ code, error:true })
);

0

สุจริต .. คุณกำลังจะไปในทางที่ผิดโดยอาศัยการฉีดเพื่อเยาะเย้ยบริการแทนโมดูล ยิ่งไปกว่านั้นการเรียกใช้ inject in beforeEach นั้นเป็นรูปแบบการต่อต้านที่ทำให้การเยาะเย้ยเป็นเรื่องยากสำหรับการทดสอบต่อครั้ง

นี่คือวิธีที่ฉันจะทำสิ่งนี้ ...

module(function ($provide) {
  // By using a decorator we can access $q and stub our method with a promise.
  $provide.decorator('myOtherService', function ($delegate, $q) {

    $delegate.makeRemoteCallReturningPromise = function () {
      var dfd = $q.defer();
      dfd.resolve('some value');
      return dfd.promise;
    };
  });
});

ตอนนี้เมื่อคุณฉีดบริการของคุณจะมีวิธีการเยาะเย้ยอย่างเหมาะสมสำหรับการใช้งาน


3
จุดก่อนหน้าของแต่ละจุดคือเรียกว่าก่อนการทดสอบแต่ละครั้งฉันไม่รู้ว่าคุณเขียนแบบทดสอบของคุณอย่างไร แต่โดยส่วนตัวแล้วฉันเขียนการทดสอบหลาย ๆ แบบสำหรับฟังก์ชั่นเดียวดังนั้นฉันจะมีการตั้งค่าพื้นฐานร่วมกัน การทดสอบแต่ละครั้ง นอกจากนี้คุณอาจต้องการค้นหาความหมายที่เข้าใจของรูปแบบการต่อต้านในขณะที่มันเกี่ยวข้องกับวิศวกรรมซอฟต์แวร์
Darren Corbett

0

ฉันพบว่ามีประโยชน์และฟังก์ชั่นบริการการแทงเป็น sinon.stub (). ส่งคืน ($ q.when ({})):

this.myService = {
   myFunction: sinon.stub().returns( $q.when( {} ) )
};

this.scope = $rootScope.$new();
this.angularStubs = {
    myService: this.myService,
    $scope: this.scope
};
this.ctrl = $controller( require( 'app/bla/bla.controller' ), this.angularStubs );

ควบคุม:

this.someMethod = function(someObj) {
   myService.myFunction( someObj ).then( function() {
        someObj.loaded = 'bla-bla';
   }, function() {
        // failure
   } );   
};

และทดสอบ

const obj = {
    field: 'value'
};
this.ctrl.someMethod( obj );

this.scope.$digest();

expect( this.myService.myFunction ).toHaveBeenCalled();
expect( obj.loaded ).toEqual( 'bla-bla' );

-1

ข้อมูลโค้ด:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.callFake(function() {
    var deferred = $q.defer();
    deferred.resolve('Remote call result');
    return deferred.promise;
});

สามารถเขียนในรูปแบบที่กระชับมากขึ้น:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue(function() {
    return $q.resolve('Remote call result');
});
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.