简体   繁体   English

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

[英]Unit testing in AngularJS - Mocking Services and Promises

In Angular everything seems to have a steep learning curve and unit testing an Angular app definitely doesn't escape this paradigm. 在Angular中,一切似乎都有一个陡峭的学习曲线,单元测试Angular应用程序肯定不会逃避这种范式。

When I started with TDD and Angular I felt that I was spending twice (maybe more) as much time figuring out just how to test and maybe even more just getting my tests set up correctly. 当我开始使用TDD和Angular时,我觉得我花了两倍(或许更多)时间来确定如何测试,甚至更多只是让我的测试设置正确。 But as Ben Nadel put it in his blog there are ups and downs in the angular learning process. 但正如Ben Nadel在他的博客中所说,角度学习过程中有起伏。 His graph is definitely my experience with Angular. 他的图表绝对是我对Angular的体验。

However as I have progressed in learning Angular and unit testing as well, now i feel that I am spending much less time setting up tests and much more time making tests go from red to green - which is a good feeling. 然而,随着我在学习Angular和单元测试方面取得了进展,现在我觉得我花费的时间少得多,设置测试的时候还有更多的时间让测试从红色变为绿色 - 这是一种很好的感觉。

So I have come across different methods of setting up my unit test to mock services and promises and I thought I would share what I have learned and also ask the question of: 所以我遇到了不同的方法来设置我的单元测试以模拟服务和承诺,我想我会分享我所学到的知识,并且还会问以下问题:

Are there any other or better ways of accomplishing this? 有没有其他或更好的方法来实现这一目标?

So onto the code, that what we all come for here anyways - not to listen to some guy talk about his love, err accomplishments learning a framework. 所以在代码上,无论如何我们都来这里 - 不是听一些人谈论他的爱,错误的成就学习框架。

This is how I started out mocking my services and promises, I'll use a controller, but services and promises can be mocked in other places obviously. 这就是我开始嘲笑我的服务和承诺的方式,我将使用一个控制器,但服务和承诺显然可以在其他地方嘲笑。

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

For us this worked, but as time went on I thought there must be a better way. 对我们来说这很有效,但随着时间的推移,我认为必须有更好的方法。 For one I hated the 对于一个我讨厌的人

promise.then.mostRecentCall 

thing , and if we wanted to reinitialise the controller then we had to pull it out of the beforeEach block and inject it individually into each test. 的事情 ,如果我们想重新初始化控制器,然后我们不得不将其拉出beforeEach块和单独注射到每个测试。

There has to be a better way... 一定有更好的方法...

Now I ask does anyone have other ways to set tests up, or and thoughts or feeling on the way I have chose to do it? 现在我问是否有人有其他方法来设置测试,或者我选择这样做的想法或感觉?

Then I came across another post, blog, stackoverflow example (you pick it I was probably there), and I saw the use of the $q library. 然后我遇到了另一个帖子,博客,stackoverflow示例(你选择它我可能在那里),我看到了使用$ q库。 Duh! 咄! Why set up a whole mock promise when we can just use the tool that Angular gives us. 当我们可以使用Angular为我们提供的工具时,为什么要建立一个完整的模拟承诺。 Our code looks nicer and makes more sense to look at - no ugly promise.then.mostRecent thing . 我们的代码看起来更好,看起来更有意义 - 没有丑陋的promise.then.mostRecent的东西

Next in the iteration of unit testing was this: 接下来在单元测试的迭代中是这样的:

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

This started to feel like the way it should be set up. 这开始感觉应该像它应该设置的方式。 We can mock what we need to mock we can set our promise to resolve and reject so we can truly test the two possible outcomes. 我们可以模拟我们需要模拟的东西,我们可以设定解决和拒绝的承诺,这样我们就可以真正测试两种可能的结果。 This feels good... 这感觉很好......

The main point in your own answer about using $q.defer sounds good. 关于使用$q.defer的答案中的$q.defer听起来不错。 My only additions would be that 我唯一的补充就是那个

setupController(0, true)

is not particularly clear, due to the parameters 0 and true , and then the if statement that uses this. 由于参数0true ,然后是使用它的if语句,因此不是特别清楚。 Also, passing the mock of products into the $controller function itself seems unusual, and means you might have 2 different products services available. 此外,将products模拟传递到$controller函数本身似乎很不寻常,这意味着您可能有2种不同的products服务可用。 One directly injected into the controller, and one injected by the usual Angular DI system into other services. 一个直接注入控制器,一个由通常的Angular DI系统注入其他服务。 I think better to use $provide to inject mocks and then everywhere in Angular will have the same instance for any test. 我认为更好地使用$provide来注入模拟,然后Angular中的任何地方都会为任何测试提供相同的实例。

Putting this all together, something like the following seems better, which can be seen at http://plnkr.co/edit/p676TYnAIb9QlD7MPIHu?p=preview 把这一切放在一起,类似下面的东西似乎更好,可以在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);
    });
  });
});

Notice that lack of if statement, and the limitation of the getProductsDeferred object, and getProducts mock, to the scope of a describe block. 请注意缺少if语句,以及getProductsDeferred对象和getProducts mock的限制,以及describe块的范围。 Using this sort of pattern, means you can add other tests, on other methods of products , without polluting the mock products object, or the setupController function you have, with all the possible methods / combinations you need for the tests. 使用这种模式意味着您可以在其他products方法上添加其他测试,而不会污染模拟products对象或您拥有的setupController函数,以及测试所需的所有可能方法/组合。

As a sidebar, I notice: 作为侧边栏,我注意到:

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

means you are separating your controllers and services into different Angular modules. 意味着您将控制器和服务分离到不同的Angular模块中。 I know certain blogs have recommended this, but I suspect this overcomplicated things, and a single module per app is ok. 我知道某些博客推荐了这个,但我怀疑这个过于复杂的事情,每个应用程序的单个模块都可以。 If you then refactor, and make services and directives completely separate reusable components, then it would be time to put them into a separate module, and use them as you would any other 3rd party module. 如果你然后重构,并使服务和指令完全分离可重用的组件,那么它是时候将它们放入一个单独的模块,并像使用任何其他第三方模块一样使用它们。

Edit: Corrected $provide.provide to $provide.value , and fixed some of the ordering of instantiation of controller/services, and added a link to Plunkr 编辑:更正$provide.provide$provide.value ,并修复了控制器/服务实例化的一些顺序,并添加了一个到Plunkr的链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM