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')
})
})
Related
I have the following observable function:
$scope.$createObservableFunction("getInformation")
.debounce(300)
.flatMapLatest(function() {
return lookupService.getInformation($scope.basicDetails);
})
.subscribe(function(info) {
$scope.info = info;
});
I am testing this by running $scope.getInformation(); in each test. I then want to check that the service has been called
it("should call the function retrieving the ABN details and new ABN details assigned", function () {
expect(lookupService.getInformation).toHaveBeenCalledWith($scope.identity.basicInformation);
});
This originally worked, but I have now added the debounce method. How can I mock this method using Jasmine 2.0.0? I don't want to use a settimeout/timer based approach
Not really an answer, but I ended up using the timer based approach.
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 am working on a code that has some unit test failing which I am trying to run. The unit test is
it('opens a confirmation dialog when deleting a layer and calls the delete wizard', function () {
var numLayers = scope.service.layers.length;
scope.deleteLayer({
serviceName: 'service1',
layerName: 'testLayer'
});
//Make sure confirmation dialog has been called
expect(bootbox.confirm).toHaveBeenCalled();
//the layer is NOT removed from the list
expect(scope.service.layers.length).toBe(numLayers);
});
I keep getting the error:
Unexpected request Get /api/configuration/utilities/userpreferences/f=json
I am trying to create a spyon for taking care of this api call. I am using this
spyOn(resourceFactory.configuration.utilities.userpreferences, 'get').and.callFake(function () { });
I have also defined this in the describe scope and injected in beforeeach
var resourceFactory = {
configuration: {
utilities: {
userpreferences: {
get: function () { }
}
}
}
};
The thing is I dont care about this call to API since I am not testing it.
When I use $httpBackend and do the following
/$httpBackend.whenGET("/api/configuration/utilities/userpreferences?f=json").respond({});
it works but I am not sure if this is the right way to do it. Especially since its not used anywhere else in the project.
Can you help with this.
I get my initial data from an outside JSON in
componentDidMount: function() { .... $.get(jsonfile, function(data) { ....
But the state of "jsonfile" changes via an input.
When the state of "jsonfile" prop changes the render method is invoked again, but I will also want to re-run the $.get request.
What would be the proper (react) way to do it?
You should abstract away your data fetching. If you put your fetching of data in a separate helper method you can call that method when needed, and it should do the fetching (and later updating) of the state
React.createClass({
componentDidMount: function () {
this.fetchData();
},
fetchData: function () {
var _this = this;
$.get('....', function (result) {
_this.setState(result);
});
},
handleClick: function () {
this.fetchData();
},
render: function () {
return (<div onClick={this.handleClick}>{data}</div>);
},
});
Please do upload some code from your project if you can.
From what I understand, you are calling an API to produce a JSON response and you have a user input to the same json?
In that case if you want to make supplementary calls to the API, you should place your API call in the correct Lifecycle Methods provided by a react component.
Please see https://facebook.github.io/react/docs/component-specs.html