As you know, inside unit tests it's built-in angularjs feature to mock XHR requests with $httpBackend - this is nice and helpful while writing unit tests.
Recently, I met with need of mocking XHR in case of file upload and discovered some problems.
Consider following code:
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", uploadProgress(event), false);
xhr.addEventListener("load", uploadComplete(event), false);
xhr.addEventListener("error", uploadError(event), false);
xhr.addEventListener("abort", uploadAbort(event), false);
xhr.open("POST", 'some url');
xhr.send(someData);
What I want to do is to do unit testing of such a code with mocking of XHR requests, but it's not possible do it because there is no $http service used here.
I tried this (and it's working and could be mocked with $httpBackend):
$http({
method: 'POST',
url: 'some url',
data: someData,
headers: {'Content-Type': undefined},
transformRequest: angular.identity})
.then(successCallback, errorCallback);
But in this case I don't know how to implement 'progress' callback and 'abort' callback (they are essential and required in case I am working on now).
I've seen information that latest Angular supports progress callback for promises (not sure though whether it's integrated with $http service), but what about abort callback?
Any ideas or maybe your met with something similar before?
If the $http service doesn't give you everything you need, you can still unit test the first block of code. First of all, change your code to use Angular's $window service. This is just a wrapper service, but it allows you to mock the object in your tests. So, you'll want to do this:
var xhr = new $window.XMLHttpRequest();
Then in your tests, just mock it and use spies.
$window.XMLHttpRequest= angular.noop;
addEventListenerSpy = jasmine.createSpy("addEventListener");
openSpy = jasmine.createSpy("open");
sendSpy = jasmine.createSpy("send");
xhrObj = {
upload:
{
addEventListener: addEventListenerSpy
},
addEventListener: addEventListenerSpy,
open: openSpy,
send: sendSpy
};
spyOn($window, "XMLHttpRequest").andReturn(xhrObj);
From there, you can make the different spies return whatever you want for the different tests.
You should mock $http and control any deferred, as you want more control over your test. Basically, mock $http provider and serve a custom implementation that exposes its deferred, then play with it.
You should not worry whether $http is working right or not, because it is supposed to, and is already tested. So you have to mock it and only worry testing your part of the code.
You should go something like this:
describe('Testing a Hello World controller', function() {
beforeEach(module(function($provide) {
$provide.provider('$http', function() {
this.$get = function($q) {
return function() {
var deferred = $q.defer(),
promise = deferred.promise;
promise.$$deferred = deferred;
return promise;
}
};
});
}));
it('should answer to fail callback', inject(function(yourService, $rootScope) {
var spyOk = jasmine.createSpy('okListener'),
spyAbort = jasmine.createSpy('abortListener'),
spyProgress = jasmine.createSpy('progressListener');
var promise = yourService.upload('a-file');
promise.then(spyOk, spyAbort, spyProgress);
promise.$$deferred.reject('something went wrong');
$rootScope.$apply();
expect(spyAbort).toHaveBeenCalledWith('something went wrong');
}));
});
And your service is simply:
app.service('yourService', function($http) {
return {
upload: function(file) {
// do something and
return $http({...});
}
};
});
Just note that promises notification is only available in the latest RC release. So, if you can't use it, just elaborate a little more the example and mock the XHR events and so.
Also note that you should preferably have one test case for each of the callbacks (fail, success and progress), in order to follow KISS principle.
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'm trying to unit test a controllers method in Angularjs that is responsible for file upload:
$scope.uploadFile = function() {
var fd = new FormData();
for (var i in $scope.files) {
fd.append("uploadedFile", $scope.files[i]);
}
var xhr = new XMLHttpRequest();
xhr.addEventListener("load", $scope.uploadComplete, false);
xhr.addEventListener("error", uploadFailed, false);
xhr.open("POST", "/fileupload");
xhr.send(fd);
}
i tried to mock the xhr object like the following :
it("using $window ", inject(function($window) {
$window.XMLHttpRequest= angular.noop;
addEventListenerSpy = jasmine.createSpy("addEventListener");
openSpy = jasmine.createSpy("open");
sendSpy = jasmine.createSpy("send");
xhrObj = {
upload:
{
addEventListener: addEventListenerSpy
},
addEventListener: addEventListenerSpy,
open: openSpy,
send: sendSpy
};
spyOn($window, "XMLHttpRequest").andReturn(xhrObj);
}));
when i run karma test config file i have the following error :
TypeError: Attempted to assign to readonly property.
at workFn (/home/dre/trunk/app/bower_components/angular-mocks/angular-mocks.js:2107)
can anyone help me i'm new in unit testing with jasmine and karma
You should be using Angular dependency injection which greatly helps tests. You should also use Angular's $http or $resource service to perform XHR requests.
Having said that, I created a fiddle with your test.
Test implementation are missing in your question, e.g. controller creation.
I hope the full example gives you a clue as to the problem in your code.
it("using $window ", function () {
xhrObj = jasmine.createSpyObj('xhrObj',
['addEventListener', 'open', 'send']);
spyOn(window, "XMLHttpRequest").andReturn(xhrObj);
scope.uploadFile()
expect(xhrObj.addEventListener).toHaveBeenCalled();
expect(xhrObj.addEventListener.calls.length).toBe(2);
});
You can find the full example here.
But I urge you to use $http/$resource instead.
Scenario
I am in the process of writing a number of jasmine tests for a Durandal based app that I am in the process of writing. The Durandal documentation suggests that the way to write tests is like
ViewModel
define([
'knockout',
'plugins/router',
'services/unitofwork',
'services/logger',
'services/errorhandler',
'services/config'
],
function (ko, router, unitofwork, logger, errorhandler, config) {
var uow = unitofwork.create();
var searchTerm = ko.observable();
var results = ko.observableArray([]);
var search = function () {
uow.myySearch(searchTerm).then(function (data) {
results(data);
logger.log(data.length + ' records found', '', 'myViewModel', true);
});
};
var vm = {
search : search,
searchTerm : searchTerm,
results : results
};
});
Test
define(['viewmodels/myViewModel'], function (myViewModel) {
describe('Stuff im testing', function(){
it('returns true', function () {
expect(true).toBe(true);
});
});
});
and for most of my tests this works great.
Problem
How do I mock/stub/fake a module that has been passed into ViewModel. For instance the UnitOfWork module so that it always returns a standard set of data.
For unit testing check out https://github.com/iammerrick/Squire.js/ a dependency mocker for requirejs. Another technique using require context is described in How can I mock dependencies for unit testing in RequireJS?.
For integration testing you might look into something like http://saucelabs.com (selenium based).
For some grunt tasks that helps setting up unit tests in phantomjs|browser see https://github.com/RainerAtSpirit/HTMLStarterKitPro (Disclaimer: I'm the maintainer of the repo). I'd love to see some mockup integration, so send a pull request if you feel inclined.
Check this out
https://github.com/danyg/jasmine-durandal
this is a library that I'm working on, in a few days will have the ability to test widgets too.
I'm using yeoman generator created app, and doing my tests in karma.
I have reusable mock objects for every of my service.
How do i correctly replace specific service dependcy with a mock, so i could then use jasmine to spy upon methods
So far i have done like this:
My service:
angular.module('ql')
.service('loginService', ['$http','API','authService', function ($http, API, authService) {
return {
//service implementation
}]);
Mock of authService:
'use strict';
//lets mock http auth service, so it would be spied upon.
ql.mock.$authServiceMockProvider = function() {
this.$get = function() {
var $service = {
loginConfirmed: function() { }
};
return $service;
};
};
//and register it.
angular.module('qlMock').provider({
$authServiceMock: ql.mock.$authServiceMockProvider
});
And my test:
'use strict';
describe('When i call login method()', function () {
// load the service's module
beforeEach(module('ql'));
beforeEach(angular.mock.module('qlMock'));
// instantiate service
var loginService,
authService,
$httpBackend;
beforeEach(function() {
// replace auth service with a mock.
// this seems kind of dirty... is there a bettery way?
module(function($provide, $injector){
authService = $injector.get('$authServiceMockProvider').$get();
$provide.value('authService', authService);
});
//actually get the loginService
/*jshint camelcase: false */
inject(function(_loginService_, _$httpBackend_) {
loginService = _loginService_;
$httpBackend =_$httpBackend_;
});
//http auth module method, that should be call only on success scenarios
spyOn(authService, 'loginConfirmed').andCallThrough();
});
it('it should do something', function () {
//actual test logic
});
});
What i do not like is the line:
authService = $injector.get('$authServiceMockProvider').$get();
I would like to simply somehow get the authServiceMock (without getting provider, and calling et method) and then inject it into loginService.
I know i could call my $authServiceMock simply authService, and provide it as a mock, so that it would always override my default implementation, but i do not want to do this.
I know this is late but maybe it will help someone who happen upon this post.
Mocking a service in Jasmine is quite simple using Angular's $provide service. The trick is to use $provide to swap out a service implementation before injecting the service.
For example let's say we are testing a service that makes use of the $location service to get information about the current URL.
// load the service's module under test
beforeEach(module('myExampleModule'));
// mock out $location with a fake one
beforeEach(module(function ($provide) {
//create mock impl
var mockLocation = {
path: function(){
return '/somewhere'
}
}
$provide.value('$location', mockLocation); // use $provide to swap the real $location with our mock
}));
var $location;
// inject dependencies ($location will be our mocked $location)
beforeEach(inject(function (_$location_) {
$location = _$location_;
}));
it('should return mock url', function(){
var path = $location.path();
expect(path).toBe('/somewhere'); //Assert that $location.path() returns '/somewhere'
});
I think I would simply use an angular service decorator to mock or totally replace your service for tests. Here is an example
I have never unit tested a service in a service, not yet anyways but our authertication/login stuff is coming up soon.
As you are unit testing the loginService you are only interested in the way the service interacts with the data it is given by the AuthService and not that the AuthService is working correctly. Which is what you have set up in the mock.
I think this would be my approach: (inside the parent describe)
var
loginService,
authService
AUTH_DATA
;
beforeEach(function() {
module('ql');
// I am assuming this is the global app module so both services live here? If not include this module as well
});
beforeEach(inject(function (_authService_, _loginService_) {
authService = _authService_;
loginService = _loginService_;
//Now with the spy setup you intercept the calls to the service and you choose what data to return, based on the unit test. Now your LoginService can simply resond to the data it is give from the login service
}));
it('it should do something', function () {
spyOn(authService, 'loginConfirmed').andReturn(AUTH_DATA);
loginService.confirmLogin(); //Dont know your actual API but a contrived guess
expect('something to happen in loginservice when AUTH_DATA is returned').toBe('Something else')
});
Background:
I'm writing unit test for angular js controllers, which utilize angular $resources wrapped in services (for maintainability purposes).
Example controller:
name = 'app.controllers.UsersIndexCtrl'
angular.module(name, [])
.controller(name, [
'$scope'
'$location'
'$dialog'
'Users'
'UserRoles'
($scope, $location, $dialog, Users, UserRoles) ->
# Get users list
$scope.users = Users.query()
# rest...
])
Example resource service:
angular.module('app.services.Users', [])
.factory 'Users', ['$rootScope', '$http', '$resource', '$location' , ($rootScope, $http, $resource, $location)->
baseUrl = '/users'
Users = $resource baseUrl + '/:userId', {userId: '#_id'}
Users.getStatus = ->
console.log 'User::getStatus()'
req = $http.get baseUrl + '/status'
req.success (res)->
$rootScope.globalUserAccountSettings = res
unless $rootScope.$$phase then $rootScope.$apply()
# other, custom methods go here...
])
Most of unit test examples in angular suggest using $httpBackend and thus mocking the $http service in controllers. To be honest, I doubt if it's a good practice since if did so I'd have to hardcode request paths in all controller tests and I want to isolate unit behaviour. $httpBackend mock is really great but only if you are using $resource in controllers directly.
A typical single test using $httpBackend would look like:
it 'should be able to add a new empty user profile', ->
$httpBackend.flush()
l = $scope.users.length
$httpBackend.expect('POST', '/users').respond _.cloneDeep mockResponseOK[0]
$scope.add()
$httpBackend.flush()
expect($scope.users.length).toEqual l + 1
What if I created a mock User resource class instance, something like:
angular.module('app.services.Users', [])
.factory 'Users', ->
class Users
$save:(cb)->
$remove:->
#query:->
#get:->
Angular DI mechanisms will override old 'app.services.Users' module with this one in a transparent way and enable me to run checks with jasmine spies.
What bothers me is the fact that I wasn't able to find a single example supporting my idea. So the question is, which one would you use and why or what am I doing wrong?
I think it makes much more sense to stub this at a service level with Jasmine spies, as you suggested. You're unit testing the controller at this point, not the service -- the exact way in which an http request is made should not be a concern for this test.
You can do something in your spec like this:
var Users = jasmine.createSpyObj('UsersStub', ['query', 'get']);
beforeEach(inject(function($provide) {
$provide.factory('Users', function(){
return Users;
});
});
And then in your relevant tests, you can stub the individual service methods to return what you expect using methods like "andCallFake" on your spy object.
The best thing you can do is make a fake resource with the methods that were suppose to be called:
var queryResponse = ['mary', 'joseph'],
Users = function() {
this.query = function() {
return queryResponse;
},
scope, HomeCtrl;
};
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
HomeCtrl = $controller('HomeCtrl', {$scope: scope, Users: new Users()});
}));
it('has users in scope', function() {
expect(scope.users).toEqual(queryResponse);
});
I'm a newbie at this stuff. I've been writing my tests using coffeescript using a dsl, but I've ran into a similar problem today. The way I solved it was by creating a jasmine spy for my resource. Then I created a promise. When the promise is resolved, it will call the 'success' function that you pass in in the controller. Then in the 'it' method, I actually resolve the promise.
I think the code would look something like this using js and jasmine, but I didn't actually have time to check
beforeEach(inject(function($rootScope, $controller, $q ) {
scope = $rootScope.$new();
queryPromise = $q.defer()
User = jasmine.createSpyObject("UsersStub", ["query"]);
User.query.andCallFake(function(success,errror){queryPromise.promise.then(success,error)});
HomeCtrl = $controller('HomeCtrl', {$scope: scope, Users: new Users()});
}));
it('has users in scope', function() {
queryPrmomise.resolve({User1: {name"joe"})
expect(scope.users).toEqual(queryResponse);
});