【问题标题】:Angular Unit Test Failing Expected spyAngular 单元测试失败预期的间谍
【发布时间】:2017-12-20 20:56:32
【问题描述】:

我有下面的控制器来获取书籍列表和单本书详细信息。它按预期工作,但单元测试没有按预期工作。

books.controller.js

var myApp = angular.module('myApp');

function BooksController($log, $routeParams, BooksService) {

    // we declare as usual, just using the `this` Object instead of `$scope`
    const vm = this;
    const routeParamId = $routeParams.id;

    if (routeParamId) {
        BooksService.getBook(routeParamId)
            .then(function (data) {
                $log.info('==> successfully fetched data for book id:', routeParamId);
                vm.book = data;
            })
            .catch(function (err) {
                vm.errorMessage = 'OOPS! Book detail not found';
                $log.error('GET BOOK: SOMETHING GOES WRONG', err)
            });
    }

    BooksService.getBooks()
        .then(function (data) {
            $log.info('==> successfully fetched data');
            vm.books = data;
        })
        .catch(function (err) {
            vm.errorMessage = 'OOPS! No books found!';
            $log.error('GET BOOK: SOMETHING GOES WRONG', err)
        });

}
BooksController.$inject = ['$log', '$routeParams', 'BooksService'];
myApp.controller('BooksController', BooksController);

上面的控制器规范,我想在其中测试 getBook(id) 服务,但不知何故我无法传递 book 的 id。

describe('Get All Books List: getBooks() =>', () => {
        const errMsg = 'OOPS! No books found!';
        beforeEach(() => {
            // injecting rootscope and controller
            inject(function (_$rootScope_, _$controller_, _$q_, BooksService) {
                $scope = _$rootScope_.$new();
                $service = BooksService;
                $q = _$q_;
                deferred = _$q_.defer();

                // Use a Jasmine Spy to return the deferred promise
                spyOn($service, 'getBooks').and.returnValue(deferred.promise);

                // The injector unwraps the underscores (_) from around the parameter names when matching
                $vm = _$controller_('BooksController', {$scope: $scope, $service: BooksService});
            });

        });

        it('should defined getBooks $http methods in booksService', () => {
            expect(typeof $service.getBooks).toEqual('function');
        });

        it('should able to fetch data from getBooks service', () => {
            // Setup the data we wish to return for the .then function in the controller
            deferred.resolve([{ id: 1 }, { id: 2 }]);

            // We have to call apply for this to work
            $scope.$apply();

            // Since we called apply, now we can perform our assertions
            expect($vm.books).not.toBe(undefined);
            expect($vm.errorMessage).toBe(undefined);
        });

        it('should print error message if data not fetched', () => {

            // Setup the data we wish to return for the .then function in the controller
            deferred.reject(errMsg);

            // We have to call apply for this to work
            $scope.$apply();

            // Since we called apply, now we can perform our assertions
            expect($vm.errorMessage).toBe(errMsg);
        });
    });

describe('Get Single Book Detail: getBook() =>', () => {
            const errMsg = 'OOPS! Book detail not found';
            const routeParamId = '59663140b6e5fe676330836c';
            beforeEach(() => {

                // injecting rootscope and controller
                inject(function (_$rootScope_, _$controller_, _$q_, BooksService) {
                    $scope = _$rootScope_.$new();
                    $scope.id = routeParamId;
                    $service = BooksService;
                    $q = _$q_;
                    var deferredSuccess = $q.defer();

                    // Use a Jasmine Spy to return the deferred promise
                    spyOn($service, 'getBook').and.returnValue(deferredSuccess.promise);
                    // The injector unwraps the underscores (_) from around the parameter names when matching
                    $vm = _$controller_('BooksController', {$scope: $scope, $service: BooksService});
                });

            });

            it('should defined getBook $http methods in booksService', () => {
                expect(typeof $service.getBook).toEqual('function');

            });

            it('should print error message', () => {
                // Setup the data we wish to return for the .then function in the controller
                deferred.reject(errMsg);

                // We have to call apply for this to work
                $scope.$apply();

                // expect($service.getBook(123)).toHaveBeenCalled();
                // expect($service.getBook(123)).toHaveBeenCalledWith(routeParamId);
                // Since we called apply, now we can perform our assertions
                expect($vm.errorMessage).toBe(errMsg);
            });
        });

“Get Single Book Detail: getBook()”这个套装不起作用。请帮助我,如何缩短这种情况。

我得到的错误如下

Chrome 59.0.3071 (Mac OS X 10.12.5) Books Controller Get Single Book Detail: getBook() => should print error message FAILED
        Expected 'OOPS! No books found!' to be 'OOPS! Book detail not found'.
Chrome 59.0.3071 (Mac OS X 10.12.5) Books Controller Get Single Book Detail: getBook() => should print error message FAILED
        Expected 'OOPS! No books found!' to be 'OOPS! Book detail not found'.
            at Object.it (test/client/controllers/books.controller.spec.js:108:38)
 Chrome 59.0.3071 (Mac OS X 10.12.5): Executed 7 of 7 (1 FAILED) (0 secs / 0.068 secs)
.
Chrome 59.0.3071 (Mac OS X 10.12.5): Executed 7 of 7 (1 FAILED) (0.005 secs / 0.068 secs)

【问题讨论】:

    标签: javascript angularjs unit-testing jasmine karma-jasmine


    【解决方案1】:

    编辑(删除原件,凌晨 2 点回答)

    您使用的是strict 模式吗?似乎存在一些范围界定问题:

    1. 在第 9 行(在“获取所有图书列表”规范中),deferred 未声明,使其隐式成为全局
    2. 在“获取所有图书列表”规范上运行的最后一个测试未通过全局 deferred 承诺
    3. 在第 60 行(在“Get Single Book Detail”规范中),deferredSuccessvar 声明,使其成为传递给 inject() 的函数的本地函数
    4. 在第 70 行(有问题的测试)上,(我假设)您的意思是拒绝“单书”deferredSuccess,您实际上没有通过 global/list deferred 承诺。这没有任何效果,因为如第 2 项所述,承诺已经失败并且Q ignores repeated rejections

    所以,这应该可以解释为什么错误不是您认为的错误。

    deferred 不是您的示例中唯一存在范围问题的变量;这些应该得到解决。我建议将文件包装在IFFE 中并使用strict mode。它将使代码更可预测并避免此类问题。

    这样做只会让你成功一半; @estus 的回复应该会完成这项工作。

    【讨论】:

    • 只是为了添加更多细节。您没有看到您期望的响应的原因是因为您已经覆盖了您正在测试的方法并明确告诉它始终返回promise.resolve。它只能返回一个空的已解决承诺。
    • 它是一个正在测试的控制器,而不是一个服务。因此必须模拟服务。
    • 更新了更正确的答案。这就是我在凌晨 2 点回答问题所得到的……
    【解决方案2】:

    真正的路由器不应该在单元测试中使用,ngRoute 模块最好从测试模块中排除。

    $scope.id = routeParamId 在控制器实例化之前分配,但根本没有使用。相反,它应该使用模拟的$routeParams 来完成。

    没有$service 服务。它被称为BooksService。因此getBooks 不是间谍。最好完全模拟服务,而不仅仅是单个方法。

    mockedBooksService = jasmine.createSpyObj('BooksService', ['getBooks']);
    
    var mockedData1 = {};
    var mockedData2 = {};
    mockedBooksService.getBooks.and.returnValues(
      $q.resolve(mockedData1),
      $q.resolve(mockedData2),
    );
    $vm = $controller('BooksController', {
      $scope: $scope,
      BooksService: mockedBooksService,
      $routeParams: { id: '59663140b6e5fe676330836c' }
    });
    
    expect(mockedBooksService.getBooks).toHaveBeenCalledTimes(2);
    expect(mockedBooksService.getBooks.calls.allArgs()).toEqual([
      ['59663140b6e5fe676330836c'], []
    ]);
    
    $rootScope.$digest();
    
    expect($vm.book).toBe(mockedData2);
    
    // then another test for falsy $routeParams.id
    

    测试揭示了控制器代码中的问题。由于在控制器构造时调用了测试代码,因此每次都应在it 中调用$controller。避免这种情况的一个好方法是将初始化代码放入可以单独测试的$onInit 方法中。

    【讨论】:

      【解决方案3】:

      你需要用提供模拟$rootScope.

      id 的值在 undefined 控制器中不可用。

      因此,非 id 条件正在执行。

         $scope = _$rootScope_.$new();
         $scope.id = routeParamId;
         module(function ($provide) {
           $provide.value('$rootScope', scope); //mock rootscope with id
         });
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-04-04
        • 2023-04-07
        • 2020-05-11
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多