Using AngularJS, CoffeeScript and Jasmine (edited in WebStorm), I would like to unit test a chain of promises.
Lets say I have the following example service:
Angular Service
class ExampleService
stepData: []
constructor: (#$http) ->
attachScopeMethod: (#scope) ->
#scope.callSteps = => #step1().then -> #step2()
step1: ->
#$http.get('app/step/1').then (results) =>
#stepData[0] = results.data
results
step2: ->
#$http.get('app/step/2').then (results) =>
#stepData[2] = results.data
results
This service allows me to attach a the method callSteps() to the scope. This method, when called, executes a series of asynch $http calls to a 3rd party API.
To test that each step is at least called, I have written the following Jasmine spec.
Jasmine Spec
ddescribe 'ExampleService', ->
beforeEach ->
module 'myApp'
beforeEach inject ($rootScope, $injector) ->
#scope = $rootScope.$new()
#exampleService = $injector.get 'exampleService'
#q = $injector.get '$q'
describe 'process example steps', ->
beforeEach ->
#exampleService.attachScopeMethod(#scope)
it "should attach the scope method", ->
expect(#scope.callSteps).toBeDefined()
describe 'when called should invoke the promise chain', ->
it "should call step1 and step2", ->
defer = #q.defer()
#exampleService.step1 = jasmine.createSpy('step1').andReturn(defer.promise)
#exampleService.step2 = jasmine.createSpy('step2')
#scope.callSteps()
defer.resolve()
expect(#exampleService.step1).toHaveBeenCalled()
expect(#exampleService.step2).toHaveBeenCalled()
The results of this test is as follows:
expect(#exampleService.step1).toHaveBeenCalled() - PASS
expect(#exampleService.step2).toHaveBeenCalled() - FAIL
Can you tell me how I can get step2() to succesfully run under test?
Thank you
EDIT
#Dashu below kindly supplied the answer to the problem. The trick is to simply call scope.$apply or scope.$digest to trigger the promise chain resolution.
So here is the working test fragment.
describe 'when called should invoke the promise chain', ->
it "should call step1 and step2", ->
defer = #q.defer()
defer.resolve()
#exampleService.step1 = jasmine.createSpy('step1').andReturn(defer.promise)
#exampleService.step2 = jasmine.createSpy('step2')
#scope.callSteps()
#scope.$apply()
expect(#exampleService.step1).toHaveBeenCalled()
expect(#exampleService.step2).toHaveBeenCalled()
try a $rootScope.$apply() before the second expect
also about defer.resolve(). i don't know if this actually resolves the promise, i think it just setups the value to return when it resolves.
so i would move that up to just underneath the $q.defer() call, before passing the promise to the andReturn()
you could do defer.resolve(true), defer.reject(false), so if you're promise will get rejected insinde callsteps, true or false will be returned
Related
I have the following test case:
it('should return id if the post is successful',function(){
var result = {
id : "123"
};
ctrl.saveCallback(result);
expect(ctrl.method.id).to.equal("123");
});
Where ctrl.saveCallback copies the result.id into the method.id on the ctrl and then shows the success banner. On the success banner, we are using the translate filter to translate the message before showing it.
Function:
.....
ctrl.method.id = result.id;
magicallyShowOnScreen($filter('translate')('MESSAGES.SUCCESS'));
....
magicallyShowOnScreen is a service that shows whatever string we pass onto the screen, and that has been injected into beforeEach.
Can someone please point in the right direction as to how should I test or mock out this $filter('translate') ?
Preface: I'm not familiar with Mocha.js or how one would create spies however you would still inject them or similar mock objects the same way as you would for other testing frameworks.
Below is a Jasmine example, I hope it helps.
When you bootstrap your module (using the module / angular.mocks.module) function, you should provide your own version of $filter which should be a mock / spy that returns another mock / spy. For example
var $filter, filterFn;
beforeEach(module('myModule', function($provide) {
$filter = jasmine.createSpy('$filter');
filterFn = jasmine.createSpy('filterFn');
$filter.and.returnValue(filterFn);
$provide.value('$filter', $filter);
});
Then, in your test, you can make sure $filter is called with the right arguments, eg
expect($filter).toHaveBeenCalledWith('translate');
expect(filterFn).toHaveBeenCalledWith('MESSAGE.SUCCESS');
expect(magicallyShowOnScreen).toHaveBeenCalled(); // assuming this too is a spy
I am trying to test if an asynchronous call returns a promise and it is failing. The function I am testing:
findOne: (collection, query) ->
#find collection, query
where find is function that simply makes a call to $http.post
The spec that is failing:
describe "#findOne", () ->
it "should return a promise", () ->
expect(entitySvc.findOne.then).toBeDefined()
Why is this failing? find is simply returning the httpPromise object returned by $http.post.
You need to actually call the method. Note the parenthases after findOne()
describe "#findOne", () ->
it "should return a promise", () ->
expect(entitySvc.findOne().then).toBeDefined()
I use $resource to set up some API calls, and while testing I have adopted the general approach of injecting $qand then doing
mockMyService.doSomethingAsync.andReturnValue($q.when(successResponse))
This has been working out pretty well, however, I have a method that looks like the following:
# MyService
MyService.doSomethingAsync(params).$promise.then ->
$scope.isLoading = false
# MyService Spec
mockMyService =
doSomethingAsync: jasmine.createSpy('doSomethingAsync')
it 'calls service #doSomethingAsync', ->
inject ($q) -> mockMyService.doSomethingAsync.and.returnValue($q.when(response))
controller.methodThatWrapsServiceCall()
scope.$apply()
expect(mockMyService.doSomethingAsync).toHaveBeenCalled()
and unfortunately the mocking strategy outlined above doesn't seem to work when a $promise.then is chained at the end. I end up with the following error:
TypeError: 'undefined' is not an object (evaluating 'MyService.doSomethingAsync(params).$promise.then')
Methods that simply end with doSomethingAsync().$promise pass the tests without issues using this mocking strategy.
(Other info: These are Jasmine tests run with Karma and PhantomJS)
Actually, figured it out!
I just changed my mock to return and.returnValue( { $promise: $q.when(response) } )
So, I'm trying to get to grips with testing angular, and I'm a bit stuck... From what I've read (or what I've understood from what I've read) the below should work, but I'm getting the following error:
Error: [ng:areq] Argument 'fn' is not a function, got Object
http://errors.angularjs.org/1.2.26/ng/areq?p0=fn&p1=not%20a%20function%2C%20got%20Object
app = angular.module("MyApp", ["ngMock"])
myService = null
angular.module("MyApp").factory "myDependency", () ->
getSomething: ->
"awesome"
angular.module("MyApp").factory "myService", (myDependency) ->
useDependency: ->
myDependency.getSomething()
describe "myService", ->
beforeEach ->
module "MyApp", ($provide) ->
mockDependency =
getSomething: ->
"mockReturnValue"
console.log "providing"
$provide.value("myDependency", mockDependency)
inject (_myService_) ->
console.log "injecting"
myService = _myService_
it "is there", ->
expect(myService).not.toBeNull()
expect(myService.useDependency()).toEq("mockReturnValue")
Also its worth saying that "provider" appears in the log, but "injecting" doesn't
Aha! I've figured it out!!!
module "MyApp", ($provide) ->
mockDependency =
getSomething: ->
"mockReturnValue"
console.log "providing"
$provide.value("myDependency", mockDependency)
return
This fixes the error! I'm assuming if a module returns something, it must be of a certain type. If its null, angular ignores it. Awesome!
I'm using AngularJS and trying to test a controller that calls a factory to get some data.
This is the controller code:
'use strict'
angular.module('AngularApp')
.controller 'IndexCtrl', ($scope, session, navigation) ->
session.find().then (response) ->
$scope.session = response.data
$scope.someOtherVariable = {}
Naturally, I'd like to swap the factory with a mock to prevent calling a real API. I'm trying to use $provide.factory to inject the mock copy:
'use strict'
describe 'Controller: IndexCtrl', ->
# load the controller's module
beforeEach module 'mosaicAdminWebClientApp'
beforeEach module ($provide) ->
$provide.factory 'session', ->
true
IndexCtrl = {}
scope = {}
# Initialize the controller and a mock scope
beforeEach inject ($controller, $rootScope) ->
scope = $rootScope.$new()
IndexCtrl = $controller 'IndexCtrl', {
$scope: scope
}
it 'should attach a list of awesomeThings to the scope', ->
expect(true).toBe true
When running this test with Karma, I guess this error:
Chrome 32.0.1700 (Mac OS X 10.9.1) Controller: IndexCtrl should attach a list of awesomeThings to the scope FAILED
Error: [ng:areq] Argument 'fn' is not a function, got Object
http://errors.angularjs.org/1.2.10-build.2176+sha.e020916/ng/areq?p0=fn&p1=not%20a%20function%2C%20got%20Object
at /Users/blaiz/Documents/some_angular_app/app/bower_components/angular/angular.js:78:12
at assertArg (/Users/blaiz/Documents/some_angular_app/app/bower_components/angular/angular.js:1363:11)
at assertArgFn (/Users/blaiz/Documents/some_angular_app/app/bower_components/angular/angular.js:1373:3)
at annotate (/Users/blaiz/Documents/some_angular_app/app/bower_components/angular/angular.js:3019:5)
at Object.invoke (/Users/blaiz/Documents/some_angular_app/app/bower_components/angular/angular.js:3685:21)
at /Users/blaiz/Documents/some_angular_app/app/bower_components/angular/angular.js:3554:71
at Array.forEach (native)
at forEach (/Users/blaiz/Documents/some_angular_app/app/bower_components/angular/angular.js:303:11)
at Object.createInjector [as injector] (/Users/blaiz/Documents/some_angular_app/app/bower_components/angular/angular.js:3554:3)
at workFn (/Users/blaiz/Documents/some_angular_app/app/bower_components/angular-mocks/angular-mocks.js:2144:52)
Chrome 32.0.1700 (Mac OS X 10.9.1): Executed 10 of 10 (1 FAILED) (0.26 secs / 0.068 secs)
Warning: Task "karma:unit" failed. Use --force to continue.
Aborted due to warnings.
I've tried many different permutation, such as removing the label, passing an object or simple value instead of a function and none of them worked.
The documentation (http://docs.angularjs.org/api/AUTO.$provide) shows that I should call the factory() method with as factory(name, $getFn) where name is a string and $getFn is a function. That's what I'm doing but it's not working.
Anything I've missed? Does anyone know how to properly use $provide in Jasmine unit tests?
Thanks
Update:
I found a plunkr similar that solved an issue similar to this one here: stackoverflow.com/questions/19297258/why-is-provide-only-available-in-the-angular-mock-module-function-and-q-onl
I created my own plunkr with this code, but with the test code in JS instead of Coffee and got it to work: plnkr.co/edit/gfBzMXpKdJgPKnoyJy5A?p=preview
Now I manually converted the JS code into Coffee: plnkr.co/edit/qGGMayFjJoeYZyFKPjuR?p=preview
The error is back so the coffee code is wrong but the js code works.
Found the answer to my question.
Since CoffeeScript always returns the result from the last statement, in this case, CoffeeScript returns $provide inside of module(), which is wrong. An easy way to fix that is to just manually add a return statement after $provide, like this:
beforeEach module 'mosaicAdminWebClientApp', ($provide) ->
$provide.service 'session', ()->
return
Hope that will help someone else.