Related
I've seen a lot of articles about how use async/await in your unit tests, but my need is the opposite.
How do you write a test for a method that uses async/await?
My spec is not able to reach any code after the 'await' line. Specifically, the spec fails in two ways.
1) HelloWorld.otherCall returns undefined instead of the return value I specify
2) HelloWorld.processResp never gets called
class HelloWorld {
async doSomething(reqObj) {
try {
const val = await this.otherCall(reqObj);
console.warn(val); // undefined
return this.processResp(val);
}
}
}
describe('HelloWorld test', function () {
let sut = new HelloWorld(); //gross simplification for demo purposes
describe('doSomething()', function () {
beforeEach(function mockInputs() {
this.resp = 'plz help - S.O.S.';
});
beforeEach(function createSpy() {
spyOn(sut, 'otherCall').and.returnValue( $q.resolve(this.resp) );
spyOn(sut, 'processResp');
});
it('should call otherCall() with proper arguments', function () {
//this test passes
});
it('should call processResp() with proper arguments', function () {
sut.doSomething({});
$rootScope.$apply(); //you need this to execute a promise chain..
expect(sut.processResp).toHaveBeenCalledWith(this.resp);
//Expected spy processResp to have been called with [ 'plz help SOS' ] but it was never called.
});
});
});
Running angular 1.5 and jasmine-core 2.6.
The .then of a promise is overloaded to handle either promises or values, and await is syntactic sugar for calling then.
So there is no reason your spy would be required to return a promise, or even a value. Returning at all, even if undefined, should trigger the await to fire, and kick off the rest of your async function.
I believe your problem is that you are not waiting for the doSomething promise to resolve before trying to test what it did. Something like this should get you more in the ballpark.
it('should call processResp() with proper arguments', async function () {
await sut.doSomething({});
// ...
});
Jasmine has Asynchronous Support. You can probably find a solution that way.
Personally, I think you should not test such methods at all.
Testing state means we're verifying that the code under test returns the right results.
Testing interactions means we're verifying that the code under test calls certain methods properly.
At most cases, testing state is better.
At your example,
async doSomething(reqObj) {
try {
const val = await this.otherCall(reqObj);
return this.processResp(val);
}
}
As long as otherCall & processResp are well covered by unit tests your good.
Do something should be covered by e2e tests.
you can read more about it at http://spectory.com/blog/Test%20Doubles%20For%20Dummies
I am migrating to the latest stable release of ui-router and am making use of the $transitions life cycle hooks to perform certain logic when certain state names are being transitioned to.
So in some of my controllers I have this kinda thing now:
this.$transitions.onStart({ }, (transition) => {
if (transition.to().name !== 'some-state-name') {
//do stuff here...
}
});
In my unit tests for the controller, previously I would broadcast a state change event on the $rootScope with the certain state names as the event args to hit the conditions I needed to test.
e.g.
$rootScope.$broadcast('$stateChangeStart', {name: 'other-state'}, {}, {}, {});
Since these state events are deprecated, whats the correct way to now trigger the $transitions.onStart(...) hooks in the tests?
I have tried just calling $state.go('some-state-name') in my tests but I can never hit my own logic within the transition hook callback function. According to the docs here, calling state.go programatically should trigger a transition, unless I am misreading?
Has anyone else managed to get unit tests for transition hooks in their controllers working for the new ui-router 1.0.x?
Full example of my controller code using a transition hook:
this.$transitions.onSuccess({ }, (transition) => {
this.setOpenItemsForState(transition.to().name);
});
test spec:
describe('stateChangeWatcher', function() {
beforeEach(function() {
spyOn(vm, 'setOpenItemsForState').and.callThrough();
});
it('should call the setOpenItemsForState method and pass it the state object', function() {
$state.go('home');
$rootScope.$apply();
expect(vm.setOpenItemsForState).toHaveBeenCalledWith('home');
});
});
My spy is never getting hit, when running the application locally this hook does get invoked as expected, so it must be something I have got setup incorrectly in my tests. Is there something extra I need to make the transition succeed in the test, since I am hooking into the onSuccess event?
Thanks
UPDATE
I raised this in the ui-router room on gitter and one of the repo contributors came back to me suggesting I check the call to $state.go('home') in my tests actually ran by adding expect($state.current.name).toBe('home'); in my test spec.
This does pass for me in my test, but I am still unable to hit the call to my function in the transition hook callback:
I'm unsure how to proceed on this, other than installing the polyfill for the legacy $stateChange events so I can use my previous code, but I'd rather not do this and figure out the proper way to test $transition hooks.
UPDATE 2
Following estus' answer, I have now stubbed out the $transitions service and also refactored my transition hook handler into a private named function in my controller:
export class NavBarController {
public static $inject = [
'$mdSidenav',
'$scope',
'$mdMedia',
'$mdComponentRegistry',
'navigationService',
'$transitions',
'$state'
];
public menuSection: Array<InterACT.Interfaces.IMenuItem>;
private openSection: InterACT.Interfaces.IMenuItem;
private openPage: InterACT.Interfaces.IMenuItem;
constructor(
private $mdSidenav,
private $scope,
private $mdMedia,
private $mdComponentRegistry,
private navigationService: NavigationService,
private $transitions: any,
private $state
) {
this.activate();
}
private activate() {
this.menuSection = this.navigationService.getNavMenu();
if (this.isScreenMedium()) {
this.$mdComponentRegistry.when('left').then(() => {
this.$mdSidenav('left').open();
});
}
this.setOpenItemsForState(this.$state.$current.name);
this.$transitions.onSuccess({ }, this.onTransitionsSuccess);
}
private onTransitionsSuccess = (transition) => {
this.setOpenItemsForState(transition.to().name);
}
private setOpenItemsForState(stateName: string) {
//stuff here...
}
}
Now in my test spec I have:
describe('Whenever a state transition succeeds', function() {
beforeEach(function() {
spyOn(vm, 'setOpenItemsForState').and.callThrough();
$state.go('home');
});
it('should call the setOpenItemsForState method passing in the name of the state that has just been transitioned to', function() {
expect($transitions.onSuccess).toHaveBeenCalledTimes(1);
expect($transitions.onSuccess.calls.mostRecent().args[0]).toEqual({});
expect($transitions.onSuccess.calls.mostRecent().args[1]).toBe(vm.onTransitionsSuccess);
});
});
These expectations pass, but Im still not able to hit my inner logic in my named hook callback onTransitionsSuccess function that make a call to setOpenItemsForState
What am I doing wrong here?
UPDATE 3
Thanks again to estu, I was forgetting I can just call my named transition hook function is a separate test:
describe('and the function bound to the transition hook callback is invoked', function(){
beforeEach(function(){
spyOn(vm, 'setOpenItemsForState');
vm.onTransitionsSuccess({
to: function(){
return {name: 'another-state'};
}
});
});
it('should call setOpenItemsForState', function(){
expect(vm.setOpenItemsForState).toHaveBeenCalledWith('another-state');
});
});
And now I get 100% coverage :)
Hopefully this will serve as a good reference to others who may be struggling to figure out how to test their own transition hooks.
A good unit test strategy for AngularJS routing is to stub a router entirely. Real router prevents units from being efficiently tested and provides unnecessary moving parts and unexpected behaviour. Since ngMock behaves differently than real application, such tests zcan't be considered proper integration tests either.
All router services in use should be stubbed. $stateProvider stub should reflect its basic behaviour, i.e. it should return itself on state call and should return $state stub on $get call:
let mockedStateProvider;
let mockedState;
let mockedTransitions;
beforeEach(module('app'));
beforeEach(module(($provide) => {
mockedState = jasmine.createSpyObj('$state', ['go']);
mockedStateProvider = jasmine.createSpyObj('$stateProvider', ['state', '$get']);
mockedStateProvider.state.and.returnValue(mockedStateProvider);
mockedStateProvider.$get.and.returnValue(mockedState);
$provide.provider('$state', function () { return mockedStateProvider });
}));
beforeEach(module(($provide) => {
mockedTransitions = jasmine.createSpyObj('$transitions', ['onStart', 'onSuccess']);
$provide.value('$transitions', mockedTransitions);
}));
A test-friendly way is to provide bound methods as callbacks instead of anonymous functions:
this.onTransitionStart = (transition) => { ... };
this.$transitions.onStart({ }, this.onTransitionStart);
Then stubbed methods can be just tested that they were called with proper arguments:
expect($transitions.onStart).toHaveBeenCalledTimes(1);
$transitions.onStart.mostRecent().args[0].toEqual({});
$transitions.onStart.mostRecent().args[1].toBe(this.onTransitionStart);
A callback function can be tested directly by calling it with expected arguments. This provides full coverage yet leaves some place for human error, so unit tests should be backed up with integration/e2e tests with real router.
I'm attempting to test that a value is changed to true after a promise is resolved inside $onInit. I'm following, as best I can, the example in this Stack Overflow question/answer. Here is my code:
class TestCtrl {
constructor(SearchService) {
this.testValue = false;
this.SearchService = SearchService;
}
$onInit() {
this.SearchService.getResults()
.then(function () {
this.testValue = true;
});
}
}
TestCtrl.$inject = ['SearchService'];
And here's the test I'm attempting to run (using mocha, chai, sinon):
it('should work', function() {
ctrl = $componentController('test', {
SearchService: SearchService
}, {});
sinon.stub(SearchService, 'getResults').resolves({response:{data: 'data'}});
ctrl.$onInit();
$rootScope.$apply();
ctrl.testValue.should.equal(true);
});
Should I be testing ctrl.testValue inside a then? Also, is using this example a bad idea because that example doesn't use a component with an $onInit lifecycle hook?
From what I've read, no, "don't use expect inside then in tests." But I'm not so sure based on what I've read elsewhere.
I wouldn't be surprised if I'm missing something obvious in how to test promises (maybe a stub wasn't the way to go?) and/or how to test what happens in the $onInit lifecycle hook.
If the question needs more details, please ask and I'll do my best to add them.
Edit: Checkout you $onInit method:
$onInit() {
this.SearchService.getResults()
.then(function () {
// `this` in anonymous function is reffering to window not the controller instance
this.testValue = true;
});
}
$onInit() {
var self = this;
self.SearchService.getResults()
.then(function () {
self.testValue = true;
});
}
Your example is correct
This is the way to test async code in angularjs - it is tested like synchronous code. Stubs' returning promises are resolved when you execute $rootScope.$apply().
Why it doesn't work
The promise returned from stub.resolves() is not an angular promise. It cannot be triggered to resolve using $rootScope, because it's not a part of angular's world. It's promise resolution queue is tied to something else and hence the need to test like you usually test async code.
Angular doesn't depend on JavaScript's native Promise implementation - it uses a light implementation of Q's promise library that is wrapped in a service called $q
The answer you have quoted uses the same service to create and return a promise from a stub
In order for your code to work - to test like you test synchronous code - you should return a $q promise (By wrapping a value in $q.when(value)) calling $rootScope.$apply() will execute the code in the then block, then proceed with the code below $rootScope.$apply() line.
Here is an example:
it('Sinon should work with angular promises', function () {
var resolved = 'resolved';
var promise = $q.when(resolved);
// Our async function
var stub = sinon.stub().returns(promise);
// Callback to be executed after the promise resolves
var handler = sinon.stub();
stub().then(handler); // async code
// The handler will be called only after $rootScope.$apply()
handler.callCount.should.equal(0);
// triggers digest which will resolve `ready` promises
// like those created with $q.when(), $q.resolve() or those created
// using the $q.defer() and deferred.resolve() was called
// this will execute the code inside the appropriate callback for
// `then/catch/finally` for all promises and then continue
// with the code bellow -> this is why the test is considered `synchronous`
$rootScope.$apply();
// Verify the handler was called and with the expected value
handler.callCount.should.equal(1);
handler.should.have.been.calledWith(resolved);
})
Here it is in action test promise synchronously in angular
For starters, you should read up on how Mocha expects you to test async code.
To start out with the quick bits:
You are on the right path - there are just some bits missing.
Yes you should do your test inside a then.
The example you linked to is fine. Just understand it.
There is absolutely no reason to avoid asserting a test inside a then. In fact, it is usually the only way to assert the resolved value of a promise.
The main problem with your test code is it tries to assert the result before it is available (as promises resolve in a later tick, they are asynchronous).
The main problem with the code you are trying to test is that there is no way of knowing when the init function has resolved.
We can deal with #2 by waiting for the stubbed SearchService.getResults to resolve (as we control the stub in the test), but that assumes too much knowledge of the implementation of onInit, so that is a bad hack.
Instead, we fix the code in TestCtrl, simply by returning the promise in onInit:
//main code / TestCtrl
$onInit() {
return this.SearchService.getResults()
.then(function () {
this.testValue = true;
});
}
Now we can simply wait for any call to onInit to resolve before we test what has happened during its execution!
To fix your test we first add a parameter to the wrapping test function. Mocha will see this and pass in a function that you can call when your test finishes.
it('should work', function(done) {
That makes it an async test. Now lets fix the test part:
ctrl.$onInit().then( () => {
ctrl.testValue.should.equal(true);
done(); // signals to mocha that the test is finished ok
}).catch(done); // pass any errors to the callback
You might find also find this answer enlightening (upvote if it helps you out). After reading it you might also understand why Mocha also supports dropping the done callback by returning a promise from the test instead. Makes for shorter tests:
return ctrl.$onInit().then( () => {
ctrl.testValue.should.equal(true);
});
sinon.stub(SearchService, 'getResults').resolves({response:{data: 'data'}}); is not returning a promise. Use $q.
I would suggest doing this:
ctrl = $componentController('test', {
SearchService: SearchService
}, {});
let deferred =$q.defer();
deferred.resolve({response:{data: 'data'}});
sinon.stub(SearchService, 'getResults').resolves(deferred.promise);
ctrl.$onInit();
$rootScope.$apply();
ctrl.testValue.should.equal(true);
You don't need to test ctrl.testValue inside a then. And generally, I would recommend not assert inside .then() in your specs. The specs will not fail if the promise never gets resolved. That can give you a false sense of security when in reality, your tests are not doing anything. But that's just my opinion.
Your test will pass once the stub returns a promise. Ideally, I would recommend using $httpBackend if the service is making an http call.
Cheers.
I've been developing in React for a while for my work, but recently I was requested to get some applications to ~100% test coverage using Istanbul. I've wrote over 160 tests for this application alone in the past few days, but I haven't been able to cover certain parts of my code. Im having the most trouble covering AJAX calls, setTimeout callbacks, and component methods that require another component to operate properly.
I've read several SO questions to no avail, and I believe that is because I'm approaching this incorrectly. I am using Enzyme, Chai assertions, Mocha, Istanbul coverage, sinon for spies, and was considering nock since I cant get sinon fakeServer working.
Here is the component method in question:
_getCategoriesFromServer() {
const _this = this;
sdk.getJSON(_this.props.sdkPath, {
itemsperpage: 10000
}).done(function(data) {
_this.setState({
isLoaded: true,
categories: _this.state.categories.concat(data)
});
});
}
Here is the test for that component:
it('should call _getCategoriesFromServer', () => {
sinon.spy(CategoryTree.prototype, '_getCategoriesFromServer');
wrapper = mount(<CategoryTree {...props} />);
expect(CategoryTree.prototype._getCategoriesFromServer.calledOnce).to.be.true;
});
The sdk is just a module that constructs a jQuery API call using getJSON.
My test is covering the function call, but its not covering the .done callback seen here:
So my question is, how can I properly test the .done?
If anyone has an article, tutorial, video, anything that explains how to properly test component methods, I would really appreciate it!
Second question is, how can I go about testing a method that gets passed down as a prop to a child component? With the testing coverage requirement I have to have that method tested, but its only purpose is to get passed down to a child component to be used as an onClick. Which is fine, but that onClick is dependent on another AJAX call returning data IN the child component.
My initial impulse was to just use enzymes .find to locate that onClick and simulate a click event, but the element with that onClick isn't there because the AJAX call didn't bring back data in the testing environment.
If you've read this far, I salute you. And if you can help, I thank you!
You could use rewire(https://github.com/jhnns/rewire) to test your component like this:
// let's said your component is ListBox.js
var rewire = require("rewire");
var myComponent = rewire("../components/ListBox.js");
const onDone = sinon.spy()
const sdkMock = {
getJSON (uri, data) {
return this.call('get', uri, data);
},
call: function (method, uri, data) {
return { done: function(){ onDone() } }
}
};
myComponent.__set__("sdk", sdkMock);
and finally you will test if the done function get called like this:
expect(onDone.calledOnce)to.be.true
With this should work as expected. If you need more options you could see all the options of rewire in GitHub.
BABEL
If you are using babel as transpiler you need to use babel-plugin-rewire(https://github.com/speedskater/babel-plugin-rewire) you could use it like this:
sdk.js
function call(method, uri, data) {
return fetch(method, uri, data);
}
export function getJSON(uri, data) {
return this.call('get', uri, data);
}
yourTest.js
import { getJSON, __RewireAPI__ as sdkMockAPI } from 'sdk.js';
describe('api call mocking', function() {
it('should use the mocked api function', function(done) {
const onDone = sinon.spy()
sdkMockAPI.__Rewire__('call', function() {
return { done: function(){ onDone() } }
});
getJSON('../dummy.json',{ data: 'dummy data'}).done()
expect(onDone.calledOnce)to.be.true
sdkMockAPI.__ResetDependency__('call')
})
})
I'm using Typescript 2.1(developer version) to transpile async/await to ES5.
I've noticed that after I change any property which is bound to view in my async function the view isn't updated with current value, so each time I have to call $scope.$apply() at the end of function.
Example async code:
async testAsync() {
await this.$timeout(2000);
this.text = "Changed";
//$scope.$apply(); <-- would like to omit this
}
And new text value isn't shown in view after this.
Is there any workaround so I don't have to manually call $scope.$apply() every time?
The answers here are correct in that AngularJS does not know about the method so you need to 'tell' Angular about any values that have been updated.
Personally I'd use $q for asynchronous behaviour instead of using await as its "The Angular way".
You can wrap non Angular methods with $q quite easily i.e. [Note this is how I wrap all Google Maps functions as they all follow this pattern of passing in a callback to be notified of completion]
function doAThing()
{
var defer = $q.defer();
// Note that this method takes a `parameter` and a callback function
someMethod(parameter, (someValue) => {
$q.resolve(someValue)
});
return defer.promise;
}
You can then use it like so
this.doAThing().then(someValue => {
this.memberValue = someValue;
});
However if you do wish to continue with await there is a better way than using $apply, in this case, and that it to use $digest. Like so
async testAsync() {
await this.$timeout(2000);
this.text = "Changed";
$scope.$digest(); <-- This is now much faster :)
}
$scope.$digest is better in this case because $scope.$apply will perform dirty checking (Angulars method for change detection) for all bound values on all scopes, this can be expensive performance wise - especially if you have many bindings. $scope.$digest will, however, only perform checking on bound values within the current $scope making it much more performant.
This can be conveniently done with angular-async-await extension:
class SomeController {
constructor($async) {
this.testAsync = $async(this.testAsync.bind(this));
}
async testAsync() { ... }
}
As it can be seen, all it does is wrapping promise-returning function with a wrapper that calls $rootScope.$apply() afterwards.
There is no reliable way to trigger digest automatically on async function, doing this would result in hacking both the framework and Promise implementation. There is no way to do this for native async function (TypeScript es2017 target), because it relies on internal promise implementation and not Promise global. More importantly, this way would be unacceptable because this is not a behaviour that is expected by default. A developer should have full control over it and assign this behaviour explicitly.
Given that testAsync is being called multiple times, and the only place where it is called is testsAsync, automatic digest in testAsync end would result in digest spam. While a proper way would be to trigger a digest once, after testsAsync.
In this case $async would be applied only to testsAsync and not to testAsync itself:
class SomeController {
constructor($async) {
this.testsAsync = $async(this.testsAsync.bind(this));
}
private async testAsync() { ... }
async testsAsync() {
await Promise.all([this.testAsync(1), this.testAsync(2), ...]);
...
}
}
I have examined the code of angular-async-await and It seems like they are using $rootScope.$apply() to digest the expression after the async promise is resolved.
This is not a good method. You can use AngularJS original $q and with a little trick, you can achieve the best performance.
First, create a function ( e.g., factory, method)
// inject $q ...
const resolver=(asyncFunc)=>{
const deferred = $q.defer();
asyncFunc()
.then(deferred.resolve)
.catch(deferred.reject);
return deferred.promise;
}
Now, you can use it in your for instance services.
getUserInfo=()=>{
return resolver(async()=>{
const userInfo=await fetch(...);
const userAddress= await fetch (...);
return {userInfo,userAddress};
});
};
This is as efficient as using AngularJS $q and with minimal code.
As #basarat said the native ES6 Promise doesn't know about the digest cycle.
What you could do is let Typescript use $q service promise instead of the native ES6 promise.
That way you won't need to invoke $scope.$apply()
angular.module('myApp')
.run(['$window', '$q', ($window, $q) => {
$window.Promise = $q;
}]);
I've set up a fiddle showcasing the desired behavior. It can be seen here: Promises with AngularJS.
Please note that it's using a bunch of Promises which resolve after 1000ms, an async function, and a Promise.race and it still only requires 4 digest cycles (open the console).
I'll reiterate what the desired behavior was:
to allow the usage of async functions just like in native JavaScript; this means no other 3rd party libraries, like $async
to automatically trigger the minimum number of digest cycles
How was this achieved?
In ES6 we've received an awesome featured called Proxy. This object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).
This means that we can wrap the Promise into a Proxy which, when the promise gets resolved or rejected, triggers a digest cycle, only if needed. Since we need a way to trigger the digest cycle, this change is added at AngularJS run time.
function($rootScope) {
function triggerDigestIfNeeded() {
// $applyAsync acts as a debounced funciton which is exactly what we need in this case
// in order to get the minimum number of digest cycles fired.
$rootScope.$applyAsync();
};
// This principle can be used with other native JS "features" when we want to integrate
// then with AngularJS; for example, fetch.
Promise = new Proxy(Promise, {
// We are interested only in the constructor function
construct(target, argumentsList) {
return (() => {
const promise = new target(...argumentsList);
// The first thing a promise does when it gets resolved or rejected,
// is to trigger a digest cycle if needed
promise.then((value) => {
triggerDigestIfNeeded();
return value;
}, (reason) => {
triggerDigestIfNeeded();
return reason;
});
return promise;
})();
}
});
}
Since async functions rely on Promises to work, the desired behavior was achieved with just a few lines of code. As an additional feature, one can use native Promises into AngularJS!
Later edit: It's not necessary to use Proxy as this behavior can be replicated with plain JS. Here it is:
Promise = ((Promise) => {
const NewPromise = function(fn) {
const promise = new Promise(fn);
promise.then((value) => {
triggerDigestIfNeeded();
return value;
}, (reason) => {
triggerDigestIfNeeded();
return reason;
});
return promise;
};
// Clone the prototype
NewPromise.prototype = Promise.prototype;
// Clone all writable instance properties
for (const propertyName of Object.getOwnPropertyNames(Promise)) {
const propertyDescription = Object.getOwnPropertyDescriptor(Promise, propertyName);
if (propertyDescription.writable) {
NewPromise[propertyName] = Promise[propertyName];
}
}
return NewPromise;
})(Promise) as any;
In case you're upgrading from AngularJS to Angular using ngUpgrade (see https://angular.io/guide/upgrade#upgrading-with-ngupgrade):
As Zone.js patches native Promises you can start rewriting all $q based AngularJS promises to native Promises, because Angular triggers a $digest automatically when the microtask queue is empty (e.g. when a Promise is resolved).
Even if you don't plan to upgrade to Angular, you can still do the same, by including Zone.js in your project and setting up a similar hook like ngUpgrade does.
Is there any workaround so I don't have to manually call $scope.$apply() every time?
This is because TypeScript uses the browser native Promise implementation and that is not what Angular 1.x knows about. To do its dirty checking all async functions that it does not control must trigger a digest cycle.
As #basarat said the native ES6 Promise doesn't know about the digest cycle. You should to promise
async testAsync() {
await this.$timeout(2000).toPromise()
.then(response => this.text = "Changed");
}
As it already has been described, angular does not know when the native Promise is finished. All async functions create a new Promise.
The possible solution can be this:
window.Promise = $q;
This way TypeScript/Babel will use angular promises instead.
Is it safe? Honestly I'm not sure - still testing this solution.
I would write a converter function, in some generic factory (didnt tested this code, but should be work)
function toNgPromise(promise)
{
var defer = $q.defer();
promise.then((data) => {
$q.resolve(data);
}).catch(response)=> {
$q.reject(response);
});
return defer.promise;
}
This is just to get you started, though I assume conversion in the end will not be as simple as this...