I'm running into a unit testing issue with Angular 2 RC5 where it doesn't appear that the ComponentFixture detectChanges method is working the way I expect it to. Here is a snippet of my code:
describe('Component Test 1', () => {
beforeEach(async(() => {
TestBed.compileComponents();
}));
it('should calculate base price', async(() => {
var fixture = TestBed.createComponent(CalcComponent);
console.log('Got here 1.');
fixture.detectChanges();
console.log('Got here 2.');
expect(fixture.componentInstance.calculateBasePrice()).toBeGreaterThan(50);
}));
});
Essentially, when this unit test is run, 'Got here 1.' is printed but the test never goes on to print the second 'Got here 2.' line, and I don't get any sort of useful error message regarding what the error might be. All I get is a very generic message telling me the test failed, even though it clearly never got to the expectation line seeing as how the second console.log line is never printed:
SUMMARY:
√ 9 tests completed
× 1 test failed
FAILED TESTS:
CalcComponent Tests
Component Test 1
× should calculate base price
PhantomJS 2.1.1 (Windows 7 0.0.0)
detectChanges
detectViewChildrenChanges
detectChangesInternal
detectChanges
detectChanges
detectViewChildrenChanges
detectChangesInternal
detectChanges
detectChanges
detectChanges
...
I know the problem is around the fixture.detectChanges() line because if I do comment it out, the second console.log prints, and I get an error regarding the expectation failing because of an undefined object (probably since fixture.detectChanges() was never called). Does anyone have an idea of what might be going on here?
Related
I'm trying to create units test for my stencil js component, in the compnentWillLoad() method it will do an HTTP request (using rxjs).when I'm run the test getting error ReferenceError: XMLHttpRequest is not defined.But when removing the HTTP request from the componentWillLoad() method test passed.
My test as below,
it('should render my component', async () => {
const page = await newSpecPage({
components: [MyComponent],
html: `<my-component></my-component>`,
});
expect(page.root).toEqualHtml(`<my-component></my-component>`);
});
I'm getting error ReferenceError: XMLHttpRequest is not defined
XMLHttpRequest is indeed not defined in the virtual DOM context that is created when you use newSpecPage.
The best solution for you is probably to write this as an E2E test instead, using newE2EPage, which is more suited for complete end-to-end testing because it runs in a real browser context where XMLHttpRequest will be available.
it('should render', async () => {
const page = await newE2EPage({ html: '<my-component></my-component>' });
const myComponent = page.find('my-component');
expect(myComponent).toHaveClass('hydrated');
});
"Spec Page" testing is rather meant for unit testing components that work stand-alone. If your goal is to actually unit-test your component and you just want to be able to instantiate your component but you don't actually need the request to succeed for testing, then you can also use the Build context from Stencil:
import { Build, ... } from '#stencil/core';
export class MyComponent {
componentWillLoad() {
if (!Build.isTesting) {
// make the request
}
}
// ...
}
I had similar troubles with Stencil, Jest and XMLHttpRequest.
First, make sure you call
new window.XMLHttpRequest()
instead of simply calling
new XMLHttpRequest()
This seems to be neccessary when using jsdom and may already resolve your issue.
It didn't resolve mine though, since I wanted to make sure there are no real API calls going on. So I tried to mock XMLHttpRequest. However, I ran into other issues while building the mock and finally decided to refactor my code to use Fetch API instead of XMLHttpRequest which seems to be better supported by Stencil.
You can easily mock fetch using jest
export function mockFetch(status, body, statusText?) {
// #ts-ignore
global.fetch = jest.fn(() =>
Promise.resolve({
status: status,
statusText: statusText,
text: () => Promise.resolve(JSON.stringify(body)),
json: () => Promise.resolve(body),
})
)
}
I am new to karma unit testing and I have encountered this problem to a simple Ionic Application.
This is my controller:
angular.module('starter').controller('AccountCtrl', function ($scope) {
$scope.settings = {
enableFriends: true
};
$scope.dummyFunction = function () {
console.log("Just do nothing!");
}
});
This is my unit testing file:
describe('AccountCtrl', function () {
var scope, createController;
beforeEach(module('starter'));
console.log("0");
beforeEach(function () {
console.log("1");
})
beforeEach(inject(function ($rootScope, $controller) {
console.log("2");
scope = $rootScope.$new();
controller = $controller('AccountCtrl', {
'$scope': scope
});
}));
it('should do stuff', function () {
console.log("3");
expect("Hello!").toBeDefined();
});
});
At this point, when I run the test I get this:
PhantomJS 2.1.1 (Mac OS X 0.0.0) LOG: 'WARNING: Tried to load angular more than once.'
PhantomJS 2.1.1 (Mac OS X 0.0.0) LOG: '0'
LOG: '1'
LOG: '3'
PhantomJS 2.1.1 (Mac OS X 0.0.0) AccountCtrl should do stuff FAILED
forEach#/Users/eduard.lache/Documents/dummyProjects/unitTestingAPP/www/lib/ionic/js/ionic.bundle.js:13696:24
loadModules#/Users/eduard.lache/Documents/dummyProjects/unitTestingAPP/www/lib/ionic/js/ionic.bundle.js:17883:12
createInjector#/Users/eduard.lache/Documents/dummyProjects/unitTestingAPP/www/lib/ionic/js/ionic.bundle.js:17805:30
workFn#/Users/eduard.lache/Documents/dummyProjects/unitTestingAPP/www/lib/angular-mocks/angular-mocks.js:2353:60
/Users/eduard.lache/Documents/dummyProjects/unitTestingAPP/www/lib/ionic/js/ionic.bundle.js:17923:53
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 1 of 1 (1 FAILED) ERROR (0.004 secs / 0.011 secs)
As you can see from the console.log messages, the inject function is not getting called and the problem is that I don't know why is that and why is this making my test fail as the test expects the string "Hello!" to be defined, which it is.
If I remove the beforeEach that contains the injection the test is successfull.
My goal is to make a test that verifies that $scope.dummyFunction() has been called and that $scope.settings is defined.
i.e.:
it('should do stuff', function () {
var ctrl = controller();
ctrl.dummyFunction();
expect(ctrl.dummyFunction).toHaveBeenCalled();
expect(ctrl.settings).toBeDefined();
});
I'm new testing Node and I just solved the same problem that you had. If it can you help, I solve this problem check all dependencies of app and put they in karma's configuration file. Then, I check that all dependencies ("dev" also) are installed and run karma, and this works! =D
I hope it helps someone.
=)
I have a keyboard shortcut directive that I'm trying to test. It's purpose is to trigger $rootScope.$broadcast() with the event data. In the application it works without any issue but I can't get this darn test to pass as thoroughly as I would like.
(function(){
'use strict';
angular
.module('app.common')
.directive('keyboardShortcuts', keyboardShortcuts);
// #ngInject
function keyboardShortcuts($document, $rootScope){
return {
restrict: 'A',
link
}
function link(scope, el, attrs){
$document.bind('keypress', event => {
console.log('detected keypress');
$rootScope.$broadcast('keyboardShortcut', event);
scope.$digest(); // seems to make no difference
});
}
}
})();
For the unit test, I've added a $scope.$on handler to show that the broadcast is actually being made and listened to, but for some reason the spy isn't doing it's job.
describe('KeyboardShortcuts Directive', function(){
'use strict';
let $element;
let $scope;
let vm;
let $document;
let $rootScope;
const mockKeyboardEvent = {
type: 'keypress',
bubbles: true,
altKey: false,
ctrlKey: false,
shiftKey: false,
which: 106
}
beforeEach(module('app.common'));
beforeEach(inject(function(_$rootScope_, $compile, _$document_){
$element = angular.element('<div keyboard-shortcuts></div>');
$rootScope = _$rootScope_.$new();
$document = _$document_;
$scope = $rootScope.$new();
$compile($element)($scope);
$scope.$digest();
spyOn($rootScope, '$broadcast');
}));
////////////
it('should broadcast when a key is pressed', function(){
$scope.$on('keyboardShortcut', (event, data) => {
console.log('wtf!?');
expect(data.which).toBe(106);
});
$document.triggerHandler(mockKeyboardEvent);
$scope.$digest(); // seems to make no difference
expect($rootScope.$broadcast).toHaveBeenCalled();
});
});
And here is the console output. You can see that the code appears to be working otherwise.
[15:33:57] Starting 'build:common:js'...
[15:33:58] Finished 'build:common:js' after 227 ms
[15:33:58] Starting 'test:common'...
20 09 2016 15:33:58.250:INFO [karma]: Karma v1.3.0 server started at http://localhost:9876/
20 09 2016 15:33:58.250:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency
20 09 2016 15:33:58.253:INFO [launcher]: Starting browser PhantomJS
20 09 2016 15:33:58.797:INFO [PhantomJS 2.1.1 (Mac OS X 0.0.0)]: Connected on socket /#37Fv2zndO5Z6XAZ8AAAg with id 3035140
..
LOG: 'detected keypress'
LOG: 'wtf!?'
PhantomJS 2.1.1 (Mac OS X 0.0.0) KeyboardShortcuts Directive should broadcast when a key is pressed FAILED
Expected spy $broadcast to have been called.
In the past, when unit testing controllers, I was having trouble like this as well. I was trying to spy on methods that were run in the activate() method which was run immediately (Papa style), I was able to get my spies to work by putting the spyOn(...) method above where the controller was instantiated. In this case, I tried putting my spyOn in all sorts of places within that 'beforeEach' and nothing has seemed to make a difference.
I also tried the obvious solution of putting the expect..broadcast..beenCalled() stuff inside of the $on handler, but that didn't work either, even though that assertion passes and is total proof that a broadcast was made.
I sort of have a feeling that the $rootScope being spied on isn't the same $rootScope that is being injected and working otherwise, but I don't know how that can be.
Proper variable naming is important.
This
$rootScope = _$rootScope_.$new();
is one of wide-spread mistakes in Angular unit tests.
The problem is that a thing that was called $rootScope is just child scope, not root scope. As it appears, it is easy to forget this, so
spyOn($rootScope, '$broadcast');
stubs $broadcast method on some scope.
It should be
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
And
$scope.$digest(); // seems to make no difference
shouldn't be there because jQuery/jqLite events aren't tied to digest cycles.
Here's a simple component:
App.FooBarComponent = Ember.Component.extend({
tagName: "button",
status: "Ready",
revertStatusPeriodMs: 2000,
click: function() {
this.set('status', 'Pending');
// A fake ajax
this.run()
.then( function() {
this.updateStatus('Finished');
}.bind(this))
.catch( function() {
this.updateStatus('Error');
}.bind(this));
},
run: function() {
return new Ember.RSVP.Promise( function(resolve) {
Ember.run.later( function() {
resolve();
}, 500);
});
},
updateStatus: function(statusText) {
this.set('status', statusText);
var periodMs = this.get('revertStatusPeriodMs') || 1000;
Ember.run.later( function() {
this.set('status', 'Ready');
}.bind(this), periodMs);
}
});
It does a simple thing: when clicked, it displays some text. Later replaces the text with another one. Even later, it reverts the text to the initial one.
The code works fine. My problem is that i'm unable to write a test for it.
test('clicking the button should set the label', function(assert) {
expect(4);
visit('/');
assert.equal( find('button').text().trim(), 'Ready', 'Should initially be "Ready"');
andThen(function() {
click('button');
assert.equal( find('button').text().trim(), 'Pending', 'Should "Pending" right after click');
});
// This one fires too late!
andThen(function() {
assert.equal( find('button').text().trim(), 'Finished', 'Should become "Finished" after promise fulfills');
});
andThen(function() {
assert.equal( find('button').text().trim(), 'Ready', 'Should eventually return to the "Ready" state');
});
});
I have two problems:
I'm unable to test the Finished state. It seems that andThen waits for all promises and run.laters to finish, while i want to test an intermediate state. How do i run an assertion between two sequential promises/run.laters?
Times can be long, and tests can take forever to complete. I generally don't mind refactoring the code for better testability, but i refuse to adjust times in the app based on the environment (e. g. 2000 for dev/prod, 0 for test). Instead, i would like to use a timer mock or some other solution.
I've tried Sinon and failed: when i mock the timer, andThen never returns. Neither of these solutions helped me.
JSBin: http://emberjs.jsbin.com/fohava/2/edit?html,js,output (Sinon is included)
None of your test code seems to use asynch – which would seem to be crucial since without anything to distinguish them, the last 2 tests will execute in the same tick (tests 3 & 4 can't both be true at the same time). I'm not familiar with QUnit, but I did find it's async method, which would seem to be pertinent. However when I tried calling assert.async, it blew up. I tried updating QUnit 1.17, still no dice.
So I don't have a solution for you. The problem, though, is that Ember will only execute both andThen tests once all the asynchronous execution has finished – this means that test 3 will never be true.
I did some code where I basically got it working but it seems really finicky and not something you would want to be do be doing..
I did this a few days ago but never replied because I wasn't happy with it - it doesn't give you enough control..
The reason I'm replying now however is because I've found a PR that may be of use to you: PR 10463
I didn't look into too much detail but apparently it "allows custom async test helpers to pause execution by returning a promise"
I'm not sure what version of Ember you're using - but if you can hold out for this PR it may help you..
I love using console log for feedback perhaps too much, and sometimes I run into code that as convention we've added $timeout in the directive/service/controller, sometimes as long as 500 ms, and now the problem is during unit test, I noticed only console.logs directly under the it constructor gets sent out to karma and output to the screen.
wrapped console logs under timeout or rather wrapped assertions under $timeout do not yield any result as if ignored, what's the solution to timeouts?
In your unit tests, you load ngMock, which overwrites the orignal $timeout with its mock. Mock $timeout doesn't work like the real JavaScript timeout. To get it to call the code that's inside it, you have to do $timeout.flush() from your unit test.
If $timeout worked like the real timeout, you would've had to write asynchronous unit-tests for all functions that use $timeout.
Here's an example of a simplified function that uses $timeout and how I test it:
gaApi.getReport = function() {
report = $q.defer()
$timeout(function() {
$http({method: 'GET', url: 'https://www.googleapis.com/analytics/v3/data/ga'})
.success(function(body) {
report.resolve(body)
})
}, 300)
return report.promise
}
A unit test:
describe('getReport', function() {
it('should return report data from Google Analytics', function() {
gaApi.getReport().then(function(body) {
expect(body.kind).toBe('analytics#gaData')
})
$timeout.flush()
$httpBackend.flush()
})
})