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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/22830391/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-27 23:50:49  来源:igfitidea点击:

Unit testing in AngularJS - Mocking Services and Promises

javascriptangularjsunit-testingtestingjasmine

提问by Sten Muchow

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. But as Ben Nadelput it in his blog there are ups and downs in the angular learning process. His graph is definitely my experience with Angular.

当我开始使用 TDD 和 Angular 时,我觉得我花了两倍(可能更多)的时间来弄清楚如何进行测试,甚至可能更多的是为了正确设置我的测试。但是正如Ben Nadel在他的博客中所说的那样,角度学习的过程有起有落。他的图表绝对是我对 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.

thing,如果我们想重新初始化控制器,那么我们必须将它从 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?

现在我问有没有人有其他方法来设置测试,或者对我选择的方式有什么想法或感受?

采纳答案by Sten Muchow

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. Duh! Why set up a whole mock promise when we can just use the tool that Angular gives us. Our code looks nicer and makes more sense to look at - no ugly promise.then.mostRecent thing.

然后我遇到了另一篇文章,博客,stackoverflow 示例(你选择它我可能在那里),我看到了 $q 库的使用。呸!当我们可以使用 Angular 提供的工具时,为什么还要设置一个完整的模拟 promise。我们的代码看起来更漂亮,看起来更有意义——没有难看的 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...

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

回答by Michal Charemza

The main point in your own answer about using $q.defersounds good. My only additions would be that

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

setupController(0, true)

is not particularly clear, due to the parameters 0and true, and then the ifstatement that uses this. Also, passing the mock of productsinto the $controllerfunction itself seems unusual, and means you might have 2 different productsservices available. One directly injected into the controller, and one injected by the usual Angular DI system into other services. I think better to use $provideto inject mocks and then everywhere in Angular will have the same instance for any test.

不是特别清楚,由于参数0and true,然后if是使用 this的语句。此外,将模拟传递products$controller函数本身似乎不寻常,这意味着您可能有 2 个不同的products服务可用。一个直接注入控制器,一个由通常的 Angular DI 系统注入到其他服务中。我认为最好使用$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 ifstatement, and the limitation of the getProductsDeferredobject, and getProductsmock, to the scope of a describeblock. Using this sort of pattern, means you can add other tests, on other methods of products, without polluting the mock productsobject, or the setupControllerfunction you have, with all the possible methods / combinations you need for the tests.

请注意缺少if语句,以及getProductsDeferred对象和getProducts模拟对describe块范围的限制。使用这种模式,意味着您可以在 的其他方法上添加其他测试,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. 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.

意味着您将控制器和服务分离到不同的 Angular 模块中。我知道某些博客已经推荐了这个,但我怀疑这过于复杂,每个应用程序一个模块是可以的。如果您随后重构,并使服务和指令完全分离可重用组件,那么是时候将它们放入一个单独的模块中,并像使用任何其他 3rd 方模块一样使用它们。

Edit: Corrected $provide.provideto $provide.value, and fixed some of the ordering of instantiation of controller/services, and added a link to Plunkr

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