Единичное тестирование в AngularJS - Mocking Services и Promises

В Angular все, кажется, имеет крутую кривую обучения, а модульное тестирование приложения Angular определенно не ускользает от этой парадигмы.

Когда я начал работу с TDD и Angular, я чувствовал, что тратил дважды (возможно, больше) столько времени, сколько выяснил, как тестировать и, возможно, даже более точно настроить мои тесты правильно. Но как Ben Nadel помещал его в свой блог, в процессе обучения Angular происходят взлеты и падения. Его график, безусловно, мой опыт работы с Angular.

Однако по мере того, как я прогрессировал в процессе обучения Angular и модульного тестирования, теперь я чувствую, что я трачу гораздо меньше времени на установление тестов и гораздо больше времени, чтобы тесты проходили от красного до зеленого - что является хорошим чувством.

Итак, я столкнулся с различными методами настройки unit test для издевательства сервисов и promises, и я подумал, что поделился бы тем, что узнал, а также задал вопрос:

Есть ли какие-либо другие или лучшие способы для этого?

Таким образом, на код, что все мы все пришли сюда, чтобы не слушать кого-то, кто говорит о своей любви, совершает ошибки, изучая рамки.

Вот как я начал издеваться над моими услугами и promises, я буду использовать контроллер, но службы и promises могут быть издевательскими в других местах, очевидно.

describe('Controller: Products', function () {
    var//iable declarations
        $scope,
        $rootScope,
        ProductsMock = {
            getProducts: function () {
            } // There might be other methods as well but I'll stick to one for the sake of consiseness
        },
        PRODUCTS = [{},{},{}]
    ;

    beforeEach(function () {
        module('App.Controllers.Products');
    });

    beforeEach(inject(function ($controller, _$rootScope_) {
        //Set up our mocked promise
        var promise = { then: jasmine.createSpy() };

        //Set up our scope
        $rootScope = _$rootScope_;
        $scope = $rootScope.$new();

        //Set up our spies
        spyOn(ProductsMock, 'getProducts').andReturn(promise);

        //Initialize the controller
        $controller('ProductsController', {
            $scope: $scope,
            Products: ProductsMock
        });

        //Resolve the promise
        promise.then.mostRecentCall.args[0](PRODUCTS);

    }));

    describe('Some Functionality', function () {
        it('should do some stuff', function () {
            expect('Stuff to happen');
        });
    });
});

Для нас это сработало, но со временем я подумал, что должен быть лучший способ. Для одного я ненавидел

promise.then.mostRecentCall 

и если бы мы хотели повторно инициализировать контроллер, нам пришлось вытащить его из блока beforeEach и вставить его в каждый тест.

Должен быть лучший способ...

Теперь я спрашиваю, есть ли у кого-нибудь другие способы тестирования тестов или мысли или чувства по тому, как я решил это сделать?

Ответ 1

Затем я наткнулся на другой пост, блог, пример stackoverflow (вы выбрали его, я, вероятно, там), и я увидел использование библиотеки $q. Duh! Зачем устанавливать целое обещание, когда мы можем просто использовать инструмент, который Angular дает нам. Наш код выглядит красивее и имеет смысл смотреть - не уродливое обещание.

Далее в итерации модульного тестирования было следующее:

describe('Controller: Products', function () {
    var//iable declarations
        $scope,
        $rootScope,
        $q,
        $controller,
        productService,
        PROMISE = {
            resolve: true,
            reject: false
        },
        PRODUCTS = [{},{},{}] //constant for the products that are returned by the service
    ;

    beforeEach(function () {
        module('App.Controllers.Products');
        module('App.Services.Products');
    });


    beforeEach(inject(function (_$controller_, _$rootScope_, _$q_, _products_) {
        $rootScope = _$rootScope_;
        $q = _$q_;
        $controller = _$controller_;
        productService = _products_;
        $scope = $rootScope.$new();
    }));

    function setupController(product, resolve) {
        //Need a function so we can setup different instances of the controller
        var getProducts = $q.defer();

        //Set up our spies
        spyOn(products, 'getProducts').andReturn(getProducts.promise);

        //Initialise the controller
        $controller('ProductsController', {
            $scope: $scope,
            products: productService
        });

        // Use $scope.$apply() to get the promise to resolve on nextTick().
        // Angular only resolves promises following a digest cycle,
        // so we manually fire one off to get the promise to resolve.
        if(resolve) {
            $scope.$apply(function() {
                getProducts.resolve();
            });
        } else {
            $scope.$apply(function() {
                getProducts.reject();
            });
        }
    }

    describe('Resolving and Rejecting the Promise', function () {
        it('should return the first PRODUCT when the promise is resolved', function () {
            setupController(PRODUCTS[0], PROMISE.resolve); // Set up our controller to return the first product and resolve the promise. 
            expect('to return the first PRODUCT when the promise is resolved');
        });

        it('should return nothing when the promise is rejected', function () {
            setupController(PRODUCTS[0], PROMISE.reject); // Set up our controller to return first product, but not to resolve the promise. 
            expect('to return nothing when the promise is rejected');
        });
    });
});

Это начало чувствовать, что он должен быть настроен. Мы можем издеваться над тем, что нам нужно, чтобы издеваться над нами, мы можем установить наше обещание разрешить и отклонить, чтобы мы могли действительно проверить два возможных результата. Это чувствует себя хорошо...

Ответ 2

Главное в вашем собственном ответе об использовании $q.defer звучит хорошо. Мое единственное дополнение -

setupController(0, true)

не является особенно ясным из-за параметров 0 и true, а затем оператора if, который использует это. Кроме того, передача макета products в функцию $controller сама по себе кажется необычной, и означает, что у вас может быть 2 различных products доступных сервиса. Один из них непосредственно вводится в контроллер и один вводится обычной системой Angular DI в другие службы. Я думаю, что лучше использовать $provide для ввода mocks, а затем всюду в Angular будет иметь тот же самый экземпляр для любого теста.

Объединяя все это, кажется, что-то вроде следующего, что можно увидеть на http://plnkr.co/edit/p676TYnAIb9QlD7MPIHu?p=preview

describe('Controller: ProductsController', function() {

  var PRODUCTS, productsMock,  $rootScope, $controller, $q;

  beforeEach(module('plunker'));

  beforeEach(module(function($provide){
    PRODUCTS = [{},{},{}]; 
    productsMock = {};        
    $provide.value('products', productsMock);
  }));

  beforeEach(inject(function (_$controller_, _$rootScope_, _$q_, _products_) {
    $rootScope = _$rootScope_;
    $q = _$q_;
    $controller = _$controller_;
    products = _products_;
  }));

  var createController = function() {
    return $controller('ProductsController', {
      $scope: $rootScope
    })
  };

  describe('on init', function() {
    var getProductsDeferred;

    var resolve = function(results) {
      getProductsDeferred.resolve(results);
      $rootScope.$apply();
    }

    var reject = function(reason) {
      getProductsDeferred.reject(reason);
      $rootScope.$apply();
    }

    beforeEach(function() {
      getProductsDeferred = $q.defer();
      productsMock.getProducts = function() {
        return getProductsDeferred.promise;
      };
      createController();
    });

    it('should set success to be true if resolved with product', function() {
      resolve(PRODUCTS[0]);
      expect($rootScope.success).toBe(true);
    });

    it('should set success to be false if rejected', function() {
      reject();
      expect($rootScope.success).toBe(false);
    });
  });
});

Обратите внимание, что отсутствие инструкции if и ограничение объекта getProductsDeferred и getProducts mock в объем блока describe. Используя этот тип шаблона, вы можете добавлять другие тесты по другим методам products, не загрязняя объект mock products или функцию setupController, которая у вас есть, со всеми возможными методами/комбинациями, которые вам нужны для тесты.

Как боковая панель, я замечаю:

module('App.Controllers.Products');
module('App.Services.Products');

означает, что вы разделяете свои контроллеры и службы на разные модули Angular. Я знаю, что некоторые блоги рекомендовали это, но я подозреваю, что это сложнейшие вещи, и один модуль для каждого приложения в порядке. Если вы затем реорганизуете и сделаете сервисы и директивы полностью разделяющими повторно используемые компоненты, тогда пришло время поместить их в отдельный модуль и использовать их, как и любой другой сторонний модуль.

Изменить: Исправлено $provide.provide до $provide.value и исправлено некоторое упорядочение создания контроллера/служб и добавлена ​​ссылка на Plunkr