【问题标题】:Test AngularJS service that returns a promise without calling $rootScope.$apply()?测试 AngularJS 服务,它返回一个承诺而不调用 $rootScope.$apply()?
【发布时间】:2014-04-12 02:15:53
【问题描述】:

我有一个返回承诺的 AngularJS 服务。

虽然代码运行良好,但测试给我带来了一些困难,因为在我的单元测试中从未调用过 promise 的“then”方法。

常见的答案似乎是在帖子“AngularJS Promise Callback Not Trigged in JasmineJS Test”中提到的调用$rootScope.$apply()。但是,如果我这样做,我的测试会尝试加载 templates/home.html,这不是预期的:

PhantomJS 1.9.7 (Linux) Controller: ScanCtrl should invoke the barcode scanner when it is available FAILED
    Error: Unexpected request: GET templates/home.html
    No more request expected

如果我没有包含$rootScope.$apply(),则永远不会调用promise 的“then”方法,并且我得到一个错误,即我的spy 没有按预期被调用:

PhantomJS 1.9.7 (Linux) Controller: ScanCtrl should invoke the barcode scanner when it is available FAILED
    Expected spy go to have been called with [ 'enterQuantity', { barcodeId : '888888888888' } ] but it was never called.

所以我的问题是 - 我应该如何让这些测试发挥作用?有什么办法可以避免拨打$rootScope.$apply()?或者,我是否需要想办法让我的代码在调用$rootScope.$apply() 时不尝试转到templates/home.html

服务

.factory('BarcodeScannerService', ['$q', function ($q) {
    return {
        scanBarcode: function () {

            var deferred = $q.defer();

            plugins.barcodeScanner.scan(
                function (result) {
                    console.log("We got a barcode\n" +
                        "Result: " + result.text + "\n" +
                        "Format: " + result.format + "\n" +
                        "Cancelled: " + result.cancelled);
                    deferred.resolve({"error": false, "barcode": result.text});

                },
                function (error) {
                    deferred.resolve({"error": true});
                });

            return deferred.promise;

        }            
    };
}]
)

单元测试

it('should invoke the barcode scanner when it is available', function () {

    inject(function ($controller, $rootScope, $q) {
        scope = $rootScope.$new();
        $rootScopeHolder = $rootScope;
        var deferred = $q.defer();

        barcodeScannerServiceMock.scanBarcode = jasmine.createSpy('scanBarcode').andReturn(deferred.promise);
        deferred.resolve({"error": false, "barcode": fakeBarcode2});

        barcodeScannerServiceMock.isAvailable = jasmine.createSpy('isAvailable').andReturn(true);

        //$scope, $timeout, Items, $state, SubmitCartService, $window
        ScanCtrl = $controller('ScanCtrl', {
            $scope: scope,
            $window: windowMock,
            Items: itemMock,
            BarcodeScannerService: barcodeScannerServiceMock,
            $state: stateMock
        });

    });

    expect(barcodeScannerServiceMock.isAvailable).toHaveBeenCalled();
    expect(barcodeScannerServiceMock.scanBarcode).toHaveBeenCalled();
    //$rootScopeHolder.$apply();
    expect(stateMock.go).toHaveBeenCalledWith('enterQuantity', { barcodeId: fakeBarcode2 });

});

被测控制器

    .controller('ScanCtrl', function ($scope, $timeout, $ionicModal, $state, BarcodeScannerService, $window) {

        $scope.handleBarcodeScanError = function () {
            var r = $window.confirm("Scanning failed.  Try again?");
            if (r === true) {
                $state.go('scan');
            }
            else {
                $state.go('home');
            }
        };

        console.log("Scanner Avaialble?" + BarcodeScannerService.isAvailable());
        if (BarcodeScannerService.isAvailable() === true) {

            var barcodeResult = {};

            BarcodeScannerService.scanBarcode()
                .then(function(result){
                    barcodeResult = result;

                    if (barcodeResult.error === false) {
                        $state.go('enterQuantity', {barcodeId: barcodeResult.barcode});
                    }

                    else {
                        $scope.handleBarcodeScanError();
                    }

                }, function(error){
                    $scope.handleBarcodeScanError();
                });

        }
        //else, if barcode scanner is not available ask them to key it in
        else {
            var tempBarcode = $window.prompt('Enter barcode:');
            $state.go('enterQuantity', {barcodeId: tempBarcode});
        }


    }
)

完整代码在这里: https://github.com/derekdata/barcode-cart-builder/

控制器:www/js/app.js 服务:www/js/services/services.js 测试:www_test/spec/controllers/ScanCtrlTest.js

提前感谢您能给我的任何见解。

【问题讨论】:

    标签: javascript angularjs jasmine ionic-framework


    【解决方案1】:

    除了使用 $rootScope.$digest$rootScope.$apply 之外,还可以使用 Q 库 (https://github.com/kriskowal/q) 作为替代品,例如:

    beforeEach(function () {
        module('Module', function ($provide) {
            $provide.value('$q', Q); 
        });
    });
    

    这种方式可以在 $digest 循环之外解决/拒绝承诺。

    【讨论】:

    • 使用 Q shim 无疑是一种选择。就我而言,我只需要基本的then/resolve/reject,我使用了一个自定义垫片:stackoverflow.com/a/39829620/2682014 也许这会有所帮助。
    【解决方案2】:

    您似乎误读了该解决方案。您需要调用 $rootScope.$digest() 而不是 $rootScope.$apply(); $digest 循环是导致 promise 检查它们是否被实现的原因。此外,您只需为当前作用域调用 $digest() 循环,因此您的实际调用将是 scope.$digest(),并置于断言之上。

    【讨论】:

    • scope.$digest() 我的断言有效。谢谢
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-05-19
    • 1970-01-01
    • 1970-01-01
    • 2020-07-30
    • 1970-01-01
    • 2015-09-21
    • 2016-02-04
    相关资源
    最近更新 更多