Внедрение макета в сервис 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 как все.

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

Спасибо

Дэвид

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

Ответы:

183

Вы можете внедрить mock-объекты в свой сервис, используя $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 за ссылку на это замечательное видео .

Джон Галамбос
источник
13
Отлично работает, но будьте осторожны: beforeEach(module('myModule'));звонок ДОЛЖЕН быть перед beforeEach(function () { MOCKING })звонком, иначе фиктивные данные будут перезаписаны реальными сервисами!
Никос Параскевопулос
1
Есть ли способ таким же образом высмеять не службу, а постоянную?
Артем
5
Подобно комментарию Никоса, любые $provideзвонки должны быть сделаны перед использованием, в $injectorпротивном случае вы получите сообщение об ошибке:Injector already created, can not register a module!
Provisionncemac
7
Что, если вашему макету требуется $ q? Тогда вы не сможете ввести $ q в макет перед вызовом module (), чтобы зарегистрировать макет. Есть предположения?
Джейк,
4
Если вы используете coffeescript и видите Error: [ng:areq] Argument 'fn' is not a function, got Object, не забудьте поставить returnна линию после $provide.value(...). Неявный возврат $provide.value(...)вызвал для меня эту ошибку.
yndolok
4

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

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

dnc253
источник
3
Я не думаю, что это ответ на вопрос. Что, если фабрика имитируемой службы сделает что-то нетривиальное, например, запросит данные на сервере? Это был бы хороший повод поиздеваться над ним. Вы хотите избежать вызова сервера и вместо этого создать фиктивную версию службы с поддельными данными. Мокинг $ http тоже не является хорошим решением, потому что тогда вы фактически тестируете две службы в одном тесте, вместо того, чтобы тестировать две службы по отдельности. Итак, я хотел бы повторить вопрос. Как передать фиктивный сервис другому сервису в модульном тесте?
Патрик Арнесен
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
        });
    });
    ....

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

tennisgent
источник
2

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

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

});
Ignitor
источник
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();
    }));
});
CodingWithSpike
источник
9
Речь идет о службах, экземпляры которых не создаются в тестовом наборе с вызовом любой эквивалентной службы, такой как $ controller. Другими словами, $ service () не вызывается в блоке beforeEach, передавая зависимости.
Моррис Сингер,
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
Почему это было отклонено? Я не понимаю ценности отрицательного голосования без комментариев.
Джейкоб Брюэр
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;
    }
Гал Морад
источник