Внедрение макета в сервис AngularJS

У меня написана служба 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 как все.

Кто-нибудь знает, как я могу имитировать внедренные сервисы для моих модульных тестов?

Спасибо

Дэйвид


person BanksySan    schedule 08.02.2013    source источник
comment
Вы можете взглянуть на этот мой ответ на другой вопрос, я надеюсь, что это может быть вам полезно.   -  person remigio    schedule 08.02.2013
comment
Также посмотрите stackoverflow.com/questions/14238490   -  person jab    schedule 29.06.2013


Ответы (7)


Вы можете внедрять имитацию в свою службу, используя $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 он также легко может быть шпионом.

Благодарим loyalBrown за ссылку на отличное видео.

person John Galambos    schedule 12.09.2013
comment
это сработало для меня, но я мог опустить beforeEach(module('myModule'));. Я использовал два beforeEach, один для предоставления, другой для внедрения службы - person danza; 15.05.2014
comment
Отлично работает, но обратите внимание на одну деталь: вызов beforeEach(module('myModule')); ДОЛЖЕН поступить перед вызовом beforeEach(function () { MOCKING }), иначе фиктивные объекты будут перезаписаны реальными службами! - person Nikos Paraskevopoulos; 16.12.2014
comment
Есть ли способ таким же образом издеваться не над службой, а над константой? - person Artem; 27.02.2015
comment
Извините, это было легко. Чтобы имитировать постоянную зависимость, нам просто нужно использовать $ .provide.constant () вместо $ provide.value (). - person Artem; 27.02.2015
comment
Подобно комментарию Никоса, любые $provide вызовы должны быть сделаны перед использованием $injector, иначе вы получите сообщение об ошибке: Injector already created, can not register a module! - person providencemac; 25.03.2015
comment
Что, если вашему макету требуется $ q? Тогда вы не сможете ввести $ q в макет перед вызовом module (), чтобы зарегистрировать макет. Есть предположения? - person Jake; 07.05.2015
comment
Если вы используете coffeescript и видите Error: [ng:areq] Argument 'fn' is not a function, got Object, не забудьте поставить return в строке после $provide.value(...). Неявный возврат $provide.value(...) вызвал у меня эту ошибку. - person yndolok; 17.07.2015
comment
А что, если один положить mockDependency в другой файл, не объявляя глобальную переменную? - person Cyril CHAPON; 08.09.2015
comment
@Jake: вы можете использовать $ provide.factory вместо $ provide.value, чтобы использовать инъекционную фабричную функцию, в которой вы можете получить $ q: $provide.factory("dep", function($q){ return {}; }) - person Oliver Hanappi; 30.03.2016
comment
Любой вызов $provide после использования $injector или inject() завершится ошибкой с указанием: Injector already created, can not register a module!. - person Alok Mishra; 30.05.2016
comment
Это также работает для служб app.services (XXXX. Сообщение об ошибке от angular не интуитивно понятно. - person cabaji99; 09.05.2017

На мой взгляд, не нужно издеваться над самими сервисами. Просто имитируйте функции сервиса. Таким образом, вы можете использовать angular для внедрения ваших реальных сервисов, как это делается во всем приложении. Затем при необходимости смоделируйте функции службы, используя функцию Jasmine spyOn.

Теперь, если сама служба является функцией, а не объектом, с которым вы можете использовать spyOn, есть другой способ сделать это. Мне нужно было это сделать, и я нашел то, что мне очень подходит. См. Как вы имитируете службу Angular, которая является функцией?

person dnc253    schedule 08.02.2013
comment
Я не думаю, что это ответ на вопрос. Что, если фабрика имитируемой службы сделает что-то нетривиальное, например, запросит данные на сервере? Это был бы хороший повод поиздеваться над ним. Вы хотите избежать вызова сервера и вместо этого создать фиктивную версию службы с поддельными данными. Мокинг $ http также не является хорошим решением, потому что тогда вы фактически тестируете две службы в одном тесте, вместо того, чтобы тестировать две службы по отдельности. Поэтому я хотел бы повторить вопрос. Как передать фиктивный сервис другому сервису в модульном тесте? - person Patrick Arnesen; 11.06.2013
comment
Если вас беспокоит, что служба обращается к серверу за данными, то для этого и нужен $ httpBackend (http://docs.angularjs.org/api/ngMock.$httpBackend). Я не уверен, что еще могло бы вызвать беспокойство на фабрике службы, которая потребовала бы издевательства над всей службой. - person dnc253; 13.06.2013

Другой способ упростить имитацию зависимостей в 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
        });
    });
    ....

Он автоматически управляет всем упомянутым выше шаблоном, поэтому вам не нужно записывать весь этот фиктивный код инъекции в каждом тесте. Надеюсь, это поможет.

person tennisgent    schedule 11.01.2015

В дополнение к ответу Джона Галамбоса: если вы просто хотите имитировать определенные методы службы , вы можете сделать это так:

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

});
person Ignitor    schedule 19.10.2015

Если ваш контроллер написан с учетом такой зависимости:

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();
    }));
});
person CodingWithSpike    schedule 09.06.2014
comment
Речь идет о службах, экземпляры которых не создаются в тестовом наборе с вызовом любой эквивалентной службы, такой как $ controller. Другими словами, нельзя вызывать $ service () в блоке beforeEach, передавая зависимости. - person Morris Singer; 26.02.2015

Недавно я выпустил 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/

person Emil van Galen    schedule 29.07.2014

Я знаю, что это старый вопрос, но есть другой более простой способ: вы можете создать макет и отключить оригинал, введенный в одну функцию, это можно сделать, используя 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;
    }
person Gal Morad    schedule 30.06.2015