AngularJS中的单元测试 - 模拟服务和承诺

时间:2014-04-03 07:13:02

标签: javascript angularjs unit-testing testing jasmine

在Angular中,一切似乎都有一个陡峭的学习曲线,单元测试Angular应用程序肯定不会逃避这种范式。

当我开始使用TDD和Angular时,我觉得我花了两倍(或许更多)时间来确定如何测试,甚至更多只是让我的测试设置正确。但正如Ben Nadel在博客中所说,角度学习过程中有起伏。他的图表绝对是我对Angular的体验。

然而,由于我在学习Angular和单元测试方面取得了进步,现在我觉得我花费的时间少得多,设置测试的时候还有更多的时间让测试从红色变为绿色 - 这是一种很好的感觉。

所以我遇到了不同的方法来设置我的单元测试以模拟服务和承诺,我想我会分享我所学到的知识,并提出以下问题:

  

还有其他或更好的方法来实现这个目标吗?

所以在代码上,无论如何我们都来这里 - 不是听一些人谈论他的爱,错误的成就学习框架。

这就是我开始嘲笑我的服务和承诺的方式,我将使用控制器,但服务和承诺显然可以在其他地方嘲笑。

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 

thing ,如果我们想重新初始化控制器,那么我们必须将它从beforeEach块中拉出来并将其单独注入每个测试中。

  

必须有更好的方法......

现在我问是否有人有其他方法来设置测试,或者我选择这样做的想法或感觉?

2 个答案:

答案 0 :(得分:12)

然后我遇到了另一个帖子,博客,stackoverflow示例(你选择它我可能在那里),我看到了使用$ q库。咄!当我们可以使用Angular为我们提供的工具时,为什么要建立一个完整的模拟承诺。我们的代码看起来更好,看起来更有意义 - 没有丑陋的promise.then.mostRecent thing

接下来在单元测试的迭代中是这样的:

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

这开始感觉就像它应该设置的方式。我们可以模拟我们需要模拟的东西,我们可以设定解决和拒绝的承诺,这样我们就可以真正测试两种可能的结果。感觉很好......

答案 1 :(得分:3)

您自己使用$q.defer的答案中的要点听起来不错。我唯一的补充就是那个

setupController(0, true)
由于参数0true,以及使用此参数的if语句,

不是特别清楚。此外,将products的模拟传递给$controller函数本身似乎并不常见,这意味着您可能有2种不同的products服务可用。一个直接注入控制器,一个由通常的Angular DI系统注入其他服务。我认为最好使用$provide来注入模拟,然后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对象和describe mock的限制缺少products块。使用这种模式意味着您可以在products的其他方法上添加其他测试,而不会使用所有可能的方法污染模拟setupController对象或module('App.Controllers.Products'); module('App.Services.Products'); 函数。 /测试所需的组合。

作为补贴,我注意到:

$provide.provide

表示您将控制器和服务分成不同的Angular模块。我知道某些博客推荐了这个,但我怀疑这个过于复杂的事情,每个应用程序的单个模块都可以。如果你然后重构,并使服务和指令完全分离可重用的组件,那么就应该把它们放到一个单独的模块中,并像使用任何其他第三方模块一样使用它们。

编辑:已将$provide.value更正为{{1}},并修复了控制器/服务实例化的一些顺序,并添加了一个指向Plunkr的链接