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

Javascript AngularJS中的单元测试-模拟服务和承诺,javascript,angularjs,unit-testing,testing,jasmine,Javascript,Angularjs,Unit Testing,Testing,Jasmine,在Angular中,一切似乎都有一个陡峭的学习曲线,Angular应用程序的单元测试肯定无法逃脱这种范式 当我开始使用TDD和Angular时,我觉得我花了两倍(可能更多)的时间来弄清楚如何测试,甚至更多的时间来正确设置测试。但正如他在博客中所说,学习过程中有起有落。他的图表绝对是我在角度方面的经验 然而,随着我在学习角度测试和单元测试方面的进步,现在我觉得我花更少的时间设置测试,花更多的时间让测试从红色变为绿色,这是一种很好的感觉 因此,我遇到了设置模拟服务和承诺的单元测试的不同方法,我想我会

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

当我开始使用TDD和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 
如果我们想重新初始化控制器,那么我们必须将其从beforeach块中拉出,并将其单独注入到每个测试中

一定有更好的方法


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

然后我看到另一篇文章,博客,stackoverflow示例(你选了它,我可能在那里),我看到了$q库的使用。哼!既然我们可以使用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');
        });
    });
});

这开始让人觉得它应该是这样设置的。我们可以模仿我们需要模仿的东西,我们可以设定承诺去解决和拒绝,这样我们就可以真正测试这两种可能的结果。这感觉很好…

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

setupController(0, true)
由于参数
0
true
,以及使用此参数的
if
语句,因此不太清楚。此外,将
产品
的模拟传递到
$controller
函数本身似乎很不寻常,这意味着您可能有两种不同的
产品
服务可用。一个直接注入控制器,另一个由普通角度DI系统注入其他服务。我认为最好使用
$provide
来注入mock,然后Angular中的每个地方都会有相同的实例用于任何测试

综上所述,下面这样的内容似乎更好,可以在

请注意,缺少
if
语句,并且
getProductsDerred
对象和
getProducts
mock的限制限制在
Descripte
块的范围内。使用这种模式,意味着您可以在
产品
的其他方法上添加其他测试,而不会污染模拟
产品
对象或您拥有的
设置控制器
功能,以及测试所需的所有可能方法/组合

作为侧边栏,我注意到:

module('App.Controllers.Products');
module('App.Services.Products');
意味着您将控制器和服务分离为不同的角度模块。我知道某些博客推荐了这一点,但我怀疑这太复杂了,每个应用程序一个模块就可以了。如果您随后进行重构,并使服务和指令完全分离可重用组件,那么就应该将它们放在单独的模块中,并像使用任何其他第三方模块一样使用它们


编辑:将
$provide.provide
更正为
$provide.value
,并修复了控制器/服务实例化的一些顺序,并添加了指向Plunkr的链接,感谢Michal花时间查看。然而,我认为您可能已经错过了实现的完整画面。我没有通过2个产品和服务。一个是全局变量,因此我们可以传递它-产品,另一个产品是从承诺返回的实际产品。最后,单词product的第三个用法是setupController fct中的一个参数,它只确定我要返回哪个产品,布尔值确定承诺是拒绝还是解决了-需要测试这两个实例。我将重命名一些变量并添加更多注释我们需要单独的模块,因为对于单元测试来说,这是理想的-如果你不理解为什么我可以更详细地讨论。但归根结底,在不相关模块的单元测试中,不需要其他模块的功能。是的,我们正在使模块与可重用组件分离。我喜欢resolve和reject的实现,但是如果我需要使用不同的设置测试控制器,该怎么办?使用您的方法,我只能使用一个控制器设置。但是,使用函数setupController,我可以选择我希望控制器的设置方式,它使代码更加简洁。我知道您没有传递2个服务,但我只想说,在测试期间使用
$provide
注入服务模拟更为常见(据我所知)。我已将一个变量重命名为
PRODUCTS[0]
,以便与您的名称更加一致。我意识到我的代码不会像您所希望的那样工作,并在response和bit.ly/OIPZoQ中进行了修复。我现在同意你仍然需要一个createController函数,我已经编辑了我的答案,包括一个:但重要的一点是它除了创建一个控制器之外什么都不做
module('App.Controllers.Products');
module('App.Services.Products');