Bagaimana cara mengejek layanan yang mengembalikan janji dalam uji unit AngularJS Jasmine?

152

saya sudah myService kegunaan itu myOtherService, yang membuat panggilan jarak jauh, mengembalikan janji:

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

      return {
        makeRemoteCall: makeRemoteCall
      };      
    }
  ])

Untuk membuat unit test myService saya perlu mengejek myOtherService, sehingga makeRemoteCallReturningPromisemetodenya memberikan janji. Beginilah cara saya melakukannya:

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

Seperti yang dapat Anda lihat di atas, definisi mock saya tergantung pada $q , yang harus saya muat menggunakan inject(). Selanjutnya, menyuntikkan mock harus terjadi module(), yang harusnya terjadi sebelumnya inject(). Namun, nilai untuk mock tidak diperbarui setelah saya mengubahnya.

Apa cara yang tepat untuk melakukan ini?

Georgii Oleinikov
sumber
Apakah kesalahannya benar-benar aktif myService.makeRemoteCall()? Jika demikian, masalahnya adalah myServicetidak memiliki makeRemoteCall, tidak ada hubungannya dengan ejekan Anda myOtherService.
dnc253
Kesalahan ada di myService.makeRemoteCall (), karena myService.myOtherService hanya objek kosong pada saat ini (nilainya tidak pernah diperbarui oleh sudut)
Georgii Oleinikov
Anda menambahkan objek kosong ke wadah ioc, setelah itu Anda mengubah referensi myOtherServiceMock untuk menunjuk ke objek baru yang Anda mata-matai. Whats dalam wadah ioc tidak akan mencerminkan hal itu, karena referensi diubah.
twDuke

Jawaban:

175

Saya tidak yakin mengapa cara Anda melakukannya tidak berhasil, tetapi saya biasanya melakukannya dengan spyOnfungsi. Sesuatu seperti ini:

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

Ingat juga bahwa Anda perlu membuat $digestpanggilan agar thenfungsi dipanggil. Lihat Pengujian bagian dari dokumentasi $ q .

------ EDIT ------

Setelah melihat lebih dekat apa yang Anda lakukan, saya pikir saya melihat masalah dalam kode Anda. Dalam beforeEach, Anda mengatur myOtherServiceMockke objek yang sama sekali baru. Tidak $provideakan pernah melihat referensi ini. Anda hanya perlu memperbarui referensi yang ada:

beforeEach(inject( function(_myService_, $q){
    myService = _myService_;
    myOtherServiceMock.makeRemoteCallReturningPromise = function() {
        var deferred = $q.defer();
        deferred.resolve('Remote call result');
        return deferred.promise;   
    };
  }
dnc253
sumber
1
Dan kau membunuhku kemarin dengan tidak muncul dalam hasil. Tampilan andCallFake () yang cantik. Terima kasih.
Priya Ranjan Singh
Alih-alih andCallFakeAnda dapat menggunakan andReturnValue(deferred.promise)(atau and.returnValue(deferred.promise)dalam Jasmine 2.0+). Anda harus menentukan deferredsebelum menelepon spyOn, tentu saja.
Jordan Running
1
Bagaimana Anda memanggil $digestdalam kasus ini ketika Anda tidak memiliki akses ke ruang lingkup?
Jim Aho
7
@ Jimho. Biasanya Anda hanya menyuntikkan $rootScopedan memanggil $digestitu.
dnc253
1
Penggunaan ditangguhkan dalam hal ini tidak perlu. Anda cukup menggunakan $q.when() codelord.net/2015/09/09/24/$q-dot-defer-youre-doing-it-wrong
fodma1
69

Kita juga bisa menulis implementasi jasmine untuk mengembalikan janji secara langsung oleh mata-mata.

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

Untuk Jasmine 2:

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

(disalin dari komentar, terima kasih kepada ccnokes)

Priya Ranjan Singh
sumber
12
Catatan untuk orang yang menggunakan Jasmine 2.0, .andReturn () telah digantikan oleh .and.returnValue. Jadi contoh di atas adalah: spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));Saya baru saja membunuh setengah jam mencari tahu itu.
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();  
    });
});
Darren Corbett
sumber
1
Beginilah cara saya melakukannya di masa lalu. Buat mata-mata yang mengembalikan barang palsu yang meniru "lalu"
Darren Corbett
Bisakah Anda memberikan contoh tes lengkap yang Anda miliki. Saya memiliki masalah serupa dengan memiliki layanan yang mengembalikan janji, tetapi dengan itu juga membuat panggilan yang mengembalikan janji!
Rob Paddock
Hai Rob, tidak yakin mengapa Anda ingin mengejek panggilan yang dibuat tiruan ke layanan lain pasti Anda ingin mengujinya saat menguji fungsi itu. Jika fungsi panggilan Anda mengejek panggilan layanan mendapat data maka mempengaruhi data bahwa janji Anda yang diejek akan mengembalikan set data yang terpengaruh palsu, setidaknya itulah yang akan saya lakukan.
Darren Corbett
Saya memulai jalur ini dan ini sangat bagus untuk skenario sederhana. Saya bahkan membuat tiruan yang mensimulasikan chaining dan menyediakan "keep" / "break" helper untuk memanggil rantai gist.github.com/marknadig/c3e8f2d3fff9d22da42b Dalam skenario yang lebih kompleks, ini jatuh namun. Dalam kasus saya, saya memiliki layanan yang akan mengembalikan item dari cache (dengan ditangguhkan) atau membuat permintaan. Jadi, itu menciptakan janjinya sendiri.
Mark Nadig
Posting ini ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy menjelaskan penggunaan "palsu" palsu secara menyeluruh.
Custodio
8

Anda dapat menggunakan pustaka mematikan seperti sinon untuk mengejek layanan Anda. Anda kemudian dapat mengembalikan $ q.when () sebagai janji Anda. Jika nilai objek lingkup Anda berasal dari hasil yang dijanjikan, Anda perlu memanggil lingkup. $ 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);


    });
Mike Lunn
sumber
2
ini adalah bagian yang hilang, menyerukan $rootScope.$digest()untuk mendapatkan janji untuk diselesaikan
2

menggunakan sinon:

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

Diketahui itu, httpPromisebisa berupa:

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

Jujur .. Anda melakukan ini dengan cara yang salah dengan mengandalkan inject untuk mengejek layanan, bukan modul. Juga, memanggil suntikan di beforeEach adalah anti-pola karena membuat mengejek sulit berdasarkan uji.

Inilah cara saya akan melakukan ini ...

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

Sekarang ketika Anda menyuntikkan layanan Anda, itu akan memiliki metode ejekan yang benar untuk penggunaan.

Keith Loy
sumber
3
Inti dari sebelum masing-masing adalah bahwa itu dipanggil sebelum setiap tes. Saya tidak tahu bagaimana Anda menulis tes Anda tetapi secara pribadi saya menulis beberapa tes untuk fungsi tunggal, oleh karena itu saya akan memiliki dasar set up yang akan dipanggil sebelum setiap tes. Anda juga mungkin ingin mencari arti pola anti dipahami karena terkait dengan rekayasa perangkat lunak.
Darren Corbett
0

Saya menemukan bahwa fungsi layanan menusuk yang bermanfaat sebagai sinon.stub (). Mengembalikan ($ 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 );

pengontrol:

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

dan uji

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

this.scope.$digest();

expect( this.myService.myFunction ).toHaveBeenCalled();
expect( obj.loaded ).toEqual( 'bla-bla' );
Dmitri Algazin
sumber
-1

Cuplikan kode:

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

Dapat ditulis dalam bentuk yang lebih ringkas:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue(function() {
    return $q.resolve('Remote call result');
});
trunikov
sumber