3

Well, my question is pretty straightforward and described in title. I have a dummy service which I want to implement in TDD fashion.

I'm going to move my implementation towards using $http service and deferred+promises. This will require $scope().$apply() in the testing code. So, as soon as I added this invocation, I observe unexpected HTTP GET calls that try to retrieve ALL template htmls that exist in my project.

PhantomJS 1.9.8 (Windows 8 0.0.0) myService should search places just fine FAILED
    Error: Unexpected request: GET app/landing/landing.html
    No more request expected
        at $httpBackend (c:/workspace/ionic/myApp/www/lib/angular-mocks/angular-mocks.js:1245)
        at sendReq (c:/workspace/ionic/myApp/www/lib/ionic/js/ionic.bundle.js:23514)
        at c:/workspace/ionic/myApp/www/lib/ionic/js/ionic.bundle.js:23225
        at processQueue (c:/workspace/ionic/myApp/www/lib/ionic/js/ionic.bundle.js:27747)
        at c:/workspace/ionic/myApp/www/lib/ionic/js/ionic.bundle.js:27763
        at c:/workspace/ionic/myApp/www/lib/ionic/js/ionic.bundle.js:29026
        at c:/workspace/ionic/myApp/www/lib/ionic/js/ionic.bundle.js:28837
        at c:/workspace/ionic/myApp/www/lib/ionic/js/ionic.bundle.js:29131
        at c:/workspace/ionic/myApp/tests/services/myServiceSpec.js:35
        at invoke (c:/workspace/ionic/myApp/www/lib/ionic/js/ionic.bundle.js:17630)
        at workFn (c:/workspace/ionic/myApp/www/lib/angular-mocks/angular-mocks.js:2439)
    undefined

Any clue why it is happening? Nooby question of angular devs I bet...

My simple service which doesn't even make $state.go() invocations.

(function () {
    'use strict';

    angular.module('myApp').factory('myService', ['$q', '$http', myService]);

    function myService($q, $http) {
        var service = this;

        function searchPlacesAsync() {
            var placesList = [
                { userName: 'Mister X', title: 'Haunted places' },
                { userName: 'Mister A', title: 'Cute places' },
                { userName: 'Mister S', title: 'Lovely places' }
            ];

            var deferred = $q.defer();
            deferred.resolve(placesList);
            return deferred.promise;
        };

        return {
            searchPlacesAsync: searchPlacesAsync
        };
    };
})();

Unit test

describe('myService', function(){
    var myService,
        $scope;

    beforeEach(module('myApp'));

    beforeEach(inject(function (_myService_, _$rootScope_){
        myService = _myService_;
        $scope = _$rootScope_.$new();
    }));

    it('should search places just fine', inject( function($httpBackend){
        // Arrange
        var placesFilter = { location: { country: 'Russia', city: 'Moscow'} };
        myService.setFilter(placesFilter);

       // TODO Why should I do it
       $httpBackend.whenGET('app/search_places/searchPlaces.html').respond({});
        //$httpBackend.whenGET('app/landing/landing.html').respond({});
        // Act
        var result = myService.searchPlacesAsync();
        $scope.$apply();
        // Assert
        expect(result).not.toBe(null);
    }) );
});

And finally, here's my Karma config file

module.exports = function(config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine'],
    files: [
        '../www/lib/ionic/js/ionic.bundle.js',
        '../www/lib/angular-mocks/angular-mocks.js',
        '../www/lib/angular/angular.js',
        '../www/lib/ionic-wizard/dist/ion-wizard.min.js',
        '../www/app/**/*.js',
        '**/*Spec.js'
    ],
    exclude: [],
    preprocessors: {},
    reporters: ['progress'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['PhantomJS'],
    singleRun: false,
    concurrency: Infinity
  })
}

UPDATE Here's my app.js file

angular.module('myApp', ['ionic', 'ionic.wizard'])

.run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {

    if (window.cordova && window.cordova.plugins.Keyboard) {
  // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
      // for form inputs)
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);

      // Don't remove this line unless you know what you are doing. It stops the viewport
      // from snapping when text inputs are focused. Ionic handles this internally for
      // a much nicer keyboard experience.
      cordova.plugins.Keyboard.disableScroll(true);
    }

    if (window.StatusBar) {
      StatusBar.styleDefault();
    }

  });
})

.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
  $stateProvider
    .state('landing', {
      // abstract: true,
      url: '/landing',
      templateUrl: 'app/landing/landing.html'
    })

    .state('user_wizard', {
      url: '/user/wizard',
      templateUrl: 'app/user_wizard/userWizard.html'
    })

    .state('provider_wizard', {
      url: '/provider/wizard',
      template: 'provider WIZARD...'
    })

    .state('search_places', {
      url: '/search/places',
      templateUrl: 'app/search_places/searchplaces.html'
    })

    .state('view_Place', {
      url: '/places/view/:id',
      templateUrl: 'app/view_place/viewPlace.html'
    });

  $urlRouterProvider.otherwise('/search/places');
}]);
Igor Soloydenko
  • 11,067
  • 11
  • 47
  • 90

1 Answers1

1

I think your main problem is that you are using $scope.$apply() to make your service return it's pending values. Instead you can just use $httpBackend.flush() to force the backend to resolve any pending requests and resolve its promises. This should deal with your issue of template fetching.

For this reason, the mock $httpBackend has a flush() method, which allows the test to explicitly flush pending requests. This preserves the async api of the backend, while allowing the test to execute synchronously.

https://docs.angularjs.org/api/ngMock/service/$httpBackend

However, you'll probably run into this again later. When you do you'll have to tell the $httpBackend what to respond to for each template. This is an impossible task to do by hand. Instead use the ng-html2js preprocessor in Karma which automatically takes your html, and compiles it into appropriate Javascript strings. You can then create a bundle of templates available as a module. I just have one giant one with all of them in it that I include in tests which need them.

Unit Testing AngularJS directive with templateUrl <== Checkout the first answer for help with the karma preprocessor

Community
  • 1
  • 1
Jazzepi
  • 5,259
  • 11
  • 55
  • 81
  • Thank you for a quick response. I replaced the `$scope.$apply()` with `$httpBackend.flush()` but this didn't affect the unit test behavior: I'm still getting this `Error: Unexpected request: GET app/landing/landing.html`. Could you give me a hint, why at all this request is happening? The service does NOT have any EXPLICIT dependencies on anything that's related to the template requested via this GET request. – Igor Soloydenko Dec 28 '15 at 02:19
  • I think you're still getting the template requests from your `_$rootScope_.$new()` Try removing that as well. – Jazzepi Dec 28 '15 at 02:21
  • Nope, this is not a part of my code anymore. That's what still confusing me. – Igor Soloydenko Dec 28 '15 at 02:30
  • @IgorSoloydenko I have to go to bed. Post your index.js if you can as well as your new code. I wonder if just including the `module('myApp')` is causing the entire thing to be evaluated and your index.js has some kind of template include in it. Actually it would probably be a big help to you if you could debug your unit test in the browser, and step through until the ajax calls for the templates are made. That way you could figure out which line is causing it. – Jazzepi Dec 28 '15 at 02:32
  • please see my post updated with app.js as you asked me. – Igor Soloydenko Dec 28 '15 at 21:36
  • 1
    I highly recommend you try the debugging step I recommended. I'm honestly pretty stumped at this point on what is causing the request. Your index.js doesn't shed any light for me unfortunately :( Unless ui-router does template requests proactively. Maybe you could try commenting out ui-router's route configuration and see if that stops it. – Jazzepi Dec 28 '15 at 21:41
  • I commented all state definitions in app.js and use `$rootScope.$digest()` in service unit test code. Now the test passes: there is no unexpected HTTP GET calls anymore. So why do these state definitions lead to these weird GET requests? I'd love to uncomment that piece of code and still be able to unit test the service... – Igor Soloydenko Dec 30 '15 at 12:05
  • Actually, you gave me the reference to a great SO question that explains why these requests happen. Thanks again – Igor Soloydenko Jan 01 '16 at 11:46