0

I have below controller to get the books list and single books detail. It's working as expected but the unit test is not working as expected.

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

Spec for above controller in which I want to test the getBook(id) service but somehow I am not able to pass the id of book.

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()" this suit is not working. Please help me, how to short out this kind of situation.

Error I am getting is below

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)
Rakesh Kumar
  • 2,705
  • 1
  • 19
  • 33

3 Answers3

0

EDIT (removed original, 2am answer)

Are you using strict mode? There appear to be a few scoping issues going on:

  1. On line 9 (in the "Get All Books List" spec), deferred is not declared, making it global implicitly
  2. The last test ran on the "Get All Books List" spec fails the global deferred promise
  3. On line 60 (in the "Get Single Book Detail" spec), deferredSuccess is declared with var making it local to the function passed to inject()
  4. On line 70 (the test in question), where (I assume) you meant to reject the "Single Book" deferredSuccess, you're actually failing the global/list deferred promise. This has no effect, since as mentioned in item 2 that promise was already failed and Q ignores repeated rejections.

So, that should explain why the error is not what you think it should be.

deferred isn't the only variable with scoping issues in your example; those should be addressed. I suggest wrapping the file in an IFFE and using strict mode. It'll make the code more predictable and avoid issues like this.

Doing this will only get you halfway there; @estus's response should round out the job.

Chris Camaratta
  • 2,729
  • 22
  • 35
  • Just to add more detail. The reason you're not seeing the response you're expecting is because you've overridden the method you're testing and explicitly told it to always return `promise.resolve`. It can't return anything but an empty resolved promise. – Chris Camaratta Jul 16 '17 at 06:14
  • It is a controller that is being tested, not a service. Thus a service has to be mocked. – Estus Flask Jul 16 '17 at 12:06
  • Updated with more correct answer. That's what I get for answering questions at 2am... – Chris Camaratta Jul 16 '17 at 19:56
0

you need to mock $rootScope. with provide.

The value of id is not getting avaibale in controller which is undefined.

So, non-id condition getting executing.

   $scope = _$rootScope_.$new();
   $scope.id = routeParamId;
   module(function ($provide) {
     $provide.value('$rootScope', scope); //mock rootscope with id
   });
RIYAJ KHAN
  • 15,032
  • 5
  • 31
  • 53
0

Real router should never be used in unit tests, with ngRoute module preferably be excluded from tested modules.

$scope.id = routeParamId is assigned before controller instantiation, but it isn't used at all. Instead, it should be done with mocked $routeParams.

There's no $service service. It's called BooksService. Thus getBooks isn't a spy. It's preferable to mock the service completely, not only a single method.

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

The test reveals the problem in controller code. Since tested code is called on controller construction, $controller should be called every time in it. A good way to avoid this is to put initialization code into $onInit method that could be tested separately.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565