Làm cách nào để tôi chế nhạo một dịch vụ trả lại lời hứa trong bài kiểm tra đơn vị AngularJS Jasmine?


152

Tôi có cách myServicesử dụng myOtherServiceđó, thực hiện cuộc gọi từ xa, trả lại lời hứa:

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

      return {
        makeRemoteCall: makeRemoteCall
      };      
    }
  ])

Để thực hiện một bài kiểm tra đơn vị cho myServicetôi cần phải chế giễu myOtherService, sao cho makeRemoteCallReturningPromisephương thức của nó trả về một lời hứa. Đây là cách tôi làm điều đó:

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

Như bạn có thể thấy ở trên, định nghĩa về giả của tôi phụ thuộc vào $q, mà tôi phải tải bằng cách sử dụng inject(). Hơn nữa, việc tiêm thuốc giả sẽ xảy ra module(), điều này sẽ đến trước inject(). Tuy nhiên, giá trị cho giả không được cập nhật khi tôi thay đổi.

Cách thích hợp để làm điều này là gì?


Là lỗi thực sự trên myService.makeRemoteCall()? Nếu vậy, vấn đề là myServicekhông có makeRemoteCall, không có gì để làm với sự chế giễu của bạn myOtherService.
dnc253

Lỗi là trên myService.makeRemoteCall (), bởi vì myService.myOtherService chỉ là một đối tượng trống rỗng tại thời điểm này (giá trị của nó không bao giờ được cập nhật bởi góc)
Georgii Oleinikov

Bạn thêm đối tượng trống vào thùng chứa ioc, sau đó bạn thay đổi tham chiếu myOtherServiceMock để trỏ đến một đối tượng mới mà bạn theo dõi. Những gì trong thùng chứa ioc sẽ không phản ánh điều đó, vì tham chiếu được thay đổi.
twDuke

Câu trả lời:


175

Tôi không chắc tại sao cách bạn làm nó không hoạt động, nhưng tôi thường làm điều đó với spyOnchức năng. Một cái gì đó như thế này:

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

Cũng nhớ rằng bạn sẽ cần thực hiện một $digestcuộc gọi cho thenchức năng được gọi. Xem phần Kiểm tra của tài liệu $ q .

------BIÊN TẬP------

Sau khi xem xét kỹ hơn những gì bạn đang làm, tôi nghĩ rằng tôi thấy vấn đề trong mã của bạn. Trong beforeEach, bạn đang thiết lập myOtherServiceMockmột đối tượng hoàn toàn mới. Các $providesẽ không bao giờ nhìn thấy thông tin này. Bạn chỉ cần cập nhật các tài liệu tham khảo hiện có:

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

1
Và bạn đã giết tôi ngày hôm qua bằng cách không hiển thị trong kết quả. Màn hình đẹp của andCallFake (). Cảm ơn bạn.
Priya Ranjan Singh

Thay vì andCallFakebạn có thể sử dụng andReturnValue(deferred.promise)(hoặc and.returnValue(deferred.promise)trong Jasmine 2.0+). Bạn cần xác định deferredtrước khi bạn gọi spyOn, tất nhiên.
Jordan Chạy

1
Làm thế nào bạn sẽ gọi $digesttrong trường hợp này khi bạn không có quyền truy cập vào phạm vi?
Jim Aho

7
@JimAho Thông thường bạn chỉ cần tiêm $rootScopevà gọi $digestvào đó.
dnc253

1
Sử dụng hoãn lại trong trường hợp này là không cần thiết. Bạn chỉ có thể sử dụng $q.when() codelord.net/2015/09/24/$q-dot-defer-youre-doing-it-wrong
fodma1

69

Chúng ta cũng có thể viết việc thực hiện lời hứa trở lại của hoa nhài bằng cách gián điệp.

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

Đối với hoa nhài 2:

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

(được sao chép từ các bình luận, nhờ ccnokes)


12
Lưu ý cho những người sử dụng Jasmine 2.0, .andReturn () đã được thay thế bằng .and.returnValue. Vì vậy, ví dụ trên sẽ là: spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));Tôi vừa giết được nửa giờ để tìm ra điều đó.
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
Đây là cách tôi đã làm nó trong quá khứ. Tạo một gián điệp trả lại một giả mạo bắt chước "sau đó"
Darren Corbett

Bạn có thể cung cấp một ví dụ về bài kiểm tra đầy đủ mà bạn có. Tôi có một vấn đề tương tự là có một dịch vụ trả lại lời hứa, nhưng trong đó cũng thực hiện một cuộc gọi trả lại lời hứa!
Rob Paddock

Xin chào Rob, không chắc tại sao bạn muốn giả lập một cuộc gọi mà một người giả mạo thực hiện cho một dịch vụ khác chắc chắn bạn sẽ muốn kiểm tra điều đó khi kiểm tra chức năng đó. Nếu hàm gọi bạn đang chế nhạo, một dịch vụ sẽ lấy dữ liệu thì ảnh hưởng đến dữ liệu đó, lời hứa bị chế giễu của bạn sẽ trả về một tập dữ liệu bị ảnh hưởng giả, ít nhất đó là cách tôi sẽ thực hiện.
Darren Corbett

Tôi bắt đầu xuống con đường này và nó hoạt động tuyệt vời cho các kịch bản đơn giản. Tôi thậm chí đã tạo ra một mô phỏng mô phỏng chuỗi và cung cấp các trình trợ giúp "giữ" / "phá vỡ" để gọi chuỗi gist.github.com/marknadig/c3e8f2d3fff9d22da42b Tuy nhiên, trong trường hợp phức tạp hơn, điều này rơi xuống. Trong trường hợp của tôi, tôi đã có một dịch vụ có điều kiện trả lại các mục từ bộ đệm (w / hoãn) hoặc đưa ra yêu cầu. Vì vậy, nó đã tạo ra lời hứa của riêng nó.
Mark Nadig

Bài đăng này ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy mô tả việc sử dụng giả mạo "sau đó" một cách xuyên suốt.
Custodio

8

Bạn có thể sử dụng một thư viện gốc như sinon để chế nhạo dịch vụ của bạn. Sau đó, bạn có thể trả lại $ q.when () như lời hứa của mình. Nếu giá trị của đối tượng phạm vi của bạn xuất phát từ kết quả lời hứa, bạn sẽ cần gọi scope. $ 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
đây là phần còn thiếu, kêu gọi $rootScope.$digest()thực hiện lời hứa

2

sử dụng sinon:

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

Được biết, httpPromisecó thể là:

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

0

Thành thật mà nói .. bạn đang đi sai hướng này bằng cách dựa vào tiêm để chế nhạo một dịch vụ thay vì mô-đun. Ngoài ra, gọi tiêm trong một BeforeEach là một kiểu chống vì nó gây khó khăn cho việc thử nghiệm trên cơ sở thử nghiệm.

Đây là cách tôi sẽ làm điều này ...

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

Bây giờ khi bạn tiêm dịch vụ của mình, nó sẽ có một phương pháp được chế giễu đúng cách để sử dụng.


3
Điểm chung của mỗi trước là nó được gọi trước mỗi bài kiểm tra Tôi không biết bạn viết bài kiểm tra của mình như thế nào nhưng cá nhân tôi viết nhiều bài kiểm tra cho một chức năng, do đó tôi sẽ có một cơ sở chung được thiết lập sẽ được gọi trước từng bài kiểm tra. Ngoài ra, bạn có thể muốn tìm kiếm ý nghĩa được hiểu của chống mẫu vì nó liên quan đến công nghệ phần mềm.
Darren Corbett

0

Tôi thấy rằng chức năng dịch vụ đâm, hữu ích là sinon.stub (). Trả về ($ 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 );

bộ điều khiển:

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

và kiểm tra

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

this.scope.$digest();

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

-1

Đoạn mã:

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

Có thể được viết dưới dạng ngắn gọn hơn:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue(function() {
    return $q.resolve('Remote call result');
});
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.