I am currently writing tests for some services using karma with jasmine, and I was wondering if I had to mock a service's service dependency that uses $http, as described below.
PS: I'm already using $httpBackend to mock any GET request, and I plan on using $httpBackend.expect* if I don't mock the service ApiProvider
This is the service I am testing
.factory('CRUDService', ['ApiProvider', function (ApiProvider) {
'use strict';
var CRUD = function CRUD(modelName) {
this.getModelName = function () {
return modelName;
};
},
overridableMethods = {
save: null
};
CRUD.prototype = {
save: function () {
// ABSTRACT
},
/**
* Deletes instance from id property
* #return http promise
*/
remove: function () {
return ApiProvider.delete(this.getModelName(), this.id);
}
};
return {
/**
* Function creating a class extending CRUD
* #param {string} modelName
* #param {Object} methods an object with methods to override, ex: save
* return {classD} the extended class
*/
build: function (modelName, methods) {
var key,
Model = function () {
};
// Class extending CRUD
Model.prototype = new CRUD(modelName);
// Apply override on methods allowed for override
for (key in methods) {
if (key in overridableMethods &&
typeof methods[key] === 'function') {
Model.prototype[key] = methods[key];
}
}
/**
* Static method
* Gets an entity of a model
* #param {Object} config #see ApiProvider config
* #return {CRUD} the reference to the entity
*/
Model.get = function (config, success) {
var entity = new Model();
ApiProvider.get(modelName, config)
.success(function (data) {
angular.extend(entity, data);
if (success) {
success();
}
});
return entity;
};
/**
* Static method
* Gets entities of a model
* #param {Object} config #see ApiProvider config
* #return {CRUD[]} the reference to the entity
*/
Model.query = function (config, success) {
var entities = [];
ApiProvider.get(modelName, config)
.success(function (data) {
data.map(function (model) {
var entity = new Model();
angular.extend(entity, model);
return entity;
});
Array.prototype.push.apply(entities, data);
if (success) {
success();
}
});
return entities;
};
return Model;
},
// Direct link to ApiProvider.post method
post: ApiProvider.post,
// Direct link to ApiProvider.put method
put: ApiProvider.put
};
}]);
And this is the service's service dependency ApiProvider
.service('ApiProvider', function ($http) {
/**
* Private
* #param {string}
* #param {object}
* #return {string} Example: /service/[config.id[/config.relatedModel], /?config.params.key1=config.params.value1&config.params.key2=config.params.value2]
*/
var buildUrl = function (service, config) {
var push = Array.prototype.push,
url = [apiRoot, service],
params = [],
param = null;
// if a key id is defined, we want to target a specific resource
if ('id' in config) {
push.apply(url, ['/', config.id]);
// a related model might be defined for this specific resource
if ('relatedModel' in config) {
push.apply(url, ['/', config.relatedModel]);
}
}
// Build query string parameters
// Please note that you can use both an array or a string for each param
// Example as an array:
// {
// queryString: {
// fields: ['field1', 'field2']
// }
// }
// Example as a string
// {
// queryString: {
// fields: 'field1,field2'
// }
// }
if ('queryString' in config) {
// loop through each key in config.params
for (paramName in config.queryString) {
// this gives us something like [my_key]=[my_value]
// and we push that string in params array
push.call(params, [paramName, '=', config.queryString[paramName]].join(''));
}
// now that all params are in an array we glue it to separate them
// so that it looks like
// ?[my_first_key]=[my_first_value]&[my_second_key]=[my_second_value]
push.apply(url, ['?', params.join('&')]);
}
return url.join('');
},
request = function (method, url, methodSpecificArgs) {
trace({
method: method,
url: url,
methodSpecificArgs: methodSpecificArgs
}, 'ApiProvider request');
return $http[method].apply($http, [url].concat(methodSpecificArgs));
},
methods = {
'get': function (url, config) {
config.cache = false;
return request('get', url, [config]);
},
'post': function (url, data, config) {
config.cache = false;
return request('post', url, [data, config]);
},
'put': function (url, data, config) {
config.cache = false;
return request('put', url, [data, config]);
},
'delete': function (url, config) {
config.cache = false;
return request('delete', url, [config]);
}
};
return {
'get': function (service, config) {
config = config || {};
return methods.get(buildUrl(service, config), config);
},
'post': function (service, data, config) {
config = config || {};
return methods.post(buildUrl(service, config), data, config);
},
'put': function (service, data, config) {
config = config || {};
return methods.put(buildUrl(service, config), data, config);
},
'delete': function (service, config) {
config = config || {};
return methods.delete(buildUrl(service, config), config);
}
};
});
Finally this is how I tested the CRUDService so far
describe('CRUDServiceTest', function () {
'use strict';
var CRUDService;
beforeEach(function () {
inject(function ($injector) {
CRUDService = $injector.get('CRUDService');
});
});
it('should have a method build', function () {
expect(CRUDService).toHaveMethod('build');
});
it('should ensure that an instance of a built service has a correct value for getModelName method',
function () {
var expectedModelName = 'myService',
BuiltService = CRUDService.build(expectedModelName),
instanceOfBuiltService = new BuiltService();
expect(instanceOfBuiltService).toHaveMethod('getModelName');
expect(instanceOfBuiltService.getModelName()).toEqual(expectedModelName);
});
// TEST get
it('should ensure build returns a class with static method get', function () {
expect(CRUDService.build()).toHaveMethod('get');
});
it('should ensure get returns an instance of CRUD', function() {
var BuiltService = CRUDService.build(),
instanceOfBuiltService = new BuiltService();
expect((BuiltService.get()).constructor).toBe(instanceOfBuiltService.constructor);
});
// TEST query
it('should ensure build returns a class with static method query', function () {
expect(CRUDService.build()).toHaveMethod('query');
});
it('should a collection of CRUD', function () {
expect(CRUDService.build()).toHaveMethod('query');
});
it('should have a static method post', function () {
expect(CRUDService).toHaveMethod('post');
});
it('should have a static method put', function () {
expect(CRUDService).toHaveMethod('put');
});
});
TLDR;
To mock or not to mock a depending service depending itself on $http ?
In general I think it's a good idea to mock out your services. If you keep up on doing it, then it makes isolating the behavior of any service you add really easy.
That being said, you don't have to at all, you can simply use Jasmine spy's.
for instance if you were testing your CRUDService which had a method like this:
remove: function () {
return ApiProvider.delete(this.getModelName(), this.id);
}
You could, in your test write something like:
var spy = spyOn(ApiProvider, 'delete').andCallFake(function(model, id) {
var def = $q.defer();
$timeout(function() { def.resolve('something'); }, 1000)
return def.promise;
});
Then if you called it:
var promise = CRUDService.remove();
expect(ApiProvider.delete).toHaveBeenCalledWith(CRUDService.getModelName(), CRUDService.id);
So basically you can mock out the functionality you need in your test, without fully mocking out the service. You can read about it more here
Hope this helped!
Related
I need to use an alias I got after intercepting a cy.wait(#..) in other tests (in different files), but I am not sure how I could do it.
However, seems like it is possible if save the data in plugins/config space and then fetch using cy.task But I am not sure how to do it. Maybe someone could help me?
I am intercepting this request
cy.intercept('POST', '**/quotes').as('abccreateQuote')
and as well I am getting the quote Id that comes in the response body.
cy.wait('#createQuote').then(({ response }) => {
if (response?.statusCode === 201) {
cy.wrap(response.body.data.id).as('abcQuoteId')
}
})
I need to use this abcQuoteId alias in different tests and located it in different files.
to visit the page
cy.visit(`quotes/${abcQuoteId}/show`)
A task would do it, but less code if you write to a fixture file
cy.wait('#createQuote').then(({ response }) => {
if (response?.statusCode === 201) {
cy.writeFile('./cypress/fixtures/abcQuoteId.json', {abcQuoteId: response.body.data.id})
}
})
Advantage over task, you can check the fixture file manually in case of typos, should look like this:
{
"abcQuoteId": 123
}
and use it like this
cy.fixture('abcQuoteId.json')
.then(fixture => {
const url = `quotes/${fixture.abcQuoteId}/show`
console.log(url) // quotes/123/show
cy.visit(url)
})
This will allow you to create infinite stores with values that will be available all the time while Cypress is running.
Each Store with it's values is available between all spec files.
Usage:
Saving some value:
// spec.file1.js
cy.wait('#createQuote').then(({ response }) => {
if (response?.statusCode === 201) {
cy.task('setItem', {
storeId: 'Global',
item: {
name: 'createQuoteResponse',
value: response.body.data.id,
},
})
}
})
Getting the above value inside another spec file:
// spec.file2.js
cy.task('getItem', {
storeId: 'Global',
item: {
name: 'createQuoteResponse',
},
}).then((item) => {
console.log(item) // Your response code
})
How to implement?
Edit:
Install cypress-store-plugin
npm install #optimumqa/cypress-store
End of Edit
May seem like a lot of code, and it is. But once setup, you won't have to modify or worry about it.
Create a ./cypress/plugins/Store.js file and paste following code:
// `./cypress/plugins/Store.js`
const StoreHelper = require('../support/Store')
const stores = {}
class Store {
constructor(on, config, pluginConfig) {
this.CONFIG = {
...{
logging: false,
},
...(pluginConfig || {}),
}
this.init(on, config, pluginConfig)
}
init(on, config, pluginConfig) {
on('task', {
/**
* #description - Store items to specific store. If store does not exist, it will be created
*
* #param {String} data.id - Store id
* #param {Object} data.item - Object containing item info
* #param {String} data.item.name - Item name
* #param {Any} data.item.value - Item value
*
* #returns {Store.Item|Null}
*/
setItem: (data) => {
let store = stores[data.storeId]
if (!store) {
stores[data.storeId] = new StoreHelper()
store = stores[data.storeId]
}
return store.setItem(data.item) || null
},
/**
* #description - Get items from specific store
*
* #param {String} data.id - Store id
* #param {Object} data.item - Object containing item info
* #param {String} data.item.name - Item name
*
* #returns {Store.Item|Null}
*/
getItem: (data) => {
const store = stores[data.storeId]
if (store) {
return store.getItem(data.item)
}
return null
},
})
}
}
module.exports = Store
Then create one more other file ./cypress/support/Store.js and paste following code in it:
// `./cypress/support/Store.js`
class Store {
constructor() {
/** #type {object} */
this.items = {}
return this
}
getItem(data = {}) {
return this.items[data.name] || null
}
setItem(data = {}) {
this.items[data.name] = new Item(data)
return this.items[data.name]
}
}
class Item {
constructor(data = {}) {
this.name = data.name || ''
this.value = data.value || undefined
return this
}
}
module.exports = Store
Cypress < v10
Inside your ./cypress/plugins/index.js require the plugin like this:
You need to require the Store file from plugins/.
// `./cypress/plugins/index.js`
module.exports = (on, config) => {
require('./Store')(on, config)
}
Cypress >= v10
// `./cypress.config.js`
const { defineConfig } = require('cypress')
const Store = require('./cypress/plugins/Store')
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
Store(on, config)
},
},
})
This is by default enabled in the cypress-boilerplate
When i created this question, my doubt was about how would i be able to test an asynchronous request utilizing mocha/enzyme/chai/sinon.
I am sure that there are different ways, but a possible one is to mock it with a handmade function that returns the appropriate values (check the answer for details).
My getInitialState method is this:
getInitialState: function() {
var me = this;
var documentData = null;
var promise = me.getDocuments();
promise.then(function(value) {
var documents = value.map(function(obj) {
return Object.keys(obj).sort().map(function(key) {
return obj[key];
});
});
documentData = documents;
});
return ({
cd: false
});
},
And the getDocuments() function that returns a promise is this:
getDocuments: function() {
var deferred = when.defer();
Collection.fetch({cd: workspaceInfo.getCD()}).then(
function(results) {
deferred.resolve(results);
},
deferred.reject
);
return deferred.promise;
},
How can i successfuly test it?
Would i have to mock the getInitialState method itself? (is that even possible)
Or just the getDocuments function with some predictable return values?
Thanks in advance, any help will be appreciated.
I solved this by requiring the Collection (which is a rest API that brings some values from a database)
var Collection = require("path/to/my/collection/Collection");
Afterwards, i use it in my getDefaultProps() method:
getDefaultProps() {
return {
Collection: new Collection()
};
},
And this in turn enables me to write tests that initialize a mocked Collection (fileDataResponse is a JSON with some data):
var CollectionMock= {
fetch: () => {
return {
then: callback => callback(fileDataResponse)
};
}
};
And use it in my test afterwards:
it("should open the modal without a loaded configuration", function() {
var instance, wrapper;
wrapper = mount(
<DocumentPreview
Collection={CollectionMock}/>
);
instance = wrapper.component.getInstance();
instance.openModal();
expect(wrapper.find('#MockedTest' + 'docOid251085').exists()).to.equal(true);
wrapper.unmount();
});
I am trying to set a variable in my controller to the return value of a function. This function is creating a new entry in a table, and then returning its id. When I debug in chrome developer tools, I can see that my function is working correctly and that response.data is in fact a number. However, when I try to set a variable to this function call, the value is being set as undefined.
My AngularJS component:
function saveNewGame($http, gameData) {
var newGameData = {
"InvestigatorGroupUserId": gameData.GroupUserId,
"InvestigatorGroupGameId": gameData.GroupGameId,
"WithTeacher": gameData.WithTeacher
};
$http.post("/APGame.WebHost/play/newGamePlayed", newGameData)
.then(function(response) {
return response.data;
});
}
function controller($http) {
var model = this;
var gameData = model.value;
var gamePlayedId;
model.startGame = function() {
gamePlayedId = saveNewGame($http, gameData);
alert(gamePlayedId);
};
}
module.component("gameApp",
{
templateUrl: "/APGame/GameAngular/game-app.html",
controllerAs: "game",
bindings: {
value: "<"
},
controller: ["$http", controller]
});
This is what my service call is doing:
[OperationContract]
[WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, UriTemplate = "newGamePlayed")]
int NewGamePlayed(GamePlayedData gamePlayedData);
public int NewGamePlayed(GamePlayedData gamePlayedData)
{
var gamePlayedRepo = _gamePlayedRepo ?? new GamePlayedRepository();
var newGame = new GamePlayed()
{
InvestigatorGroupUserId = gamePlayedData.InvestigatorGroupUserId,
InvestigatorGroupGameId = gamePlayedData.InvestigatorGroupGameId,
GameStartTime = DateTime.Now,
IsComplete = false
};
return gamePlayedRepo.Create(newGame);
}
Add a promise resolvement listener to the method invoke like following:
model.startGame = function() {
gamePlayedId = saveNewGame($http, gameData)then(function(response) {
alert(response.data);
}, function(reason) {
alert('Failed: ' + reason);
});
};
Return the http.get promise instead of the data
function saveNewGame($http, gameData) {
var newGameData = {
"InvestigatorGroupUserId": gameData.GroupUserId,
"InvestigatorGroupGameId": gameData.GroupGameId,
"WithTeacher": gameData.WithTeacher
};
return $http.post("/APGame.WebHost/play/newGamePlayed", newGameData);
}
The reason is because your function is not returning any value thus undefined.
$http.post("/APGame.WebHost/play/newGamePlayed", newGameData)
.then(function(response) {
// notice that you are returning this value to the function(response) not your saveNewGame function
return response.data;
});
Due to asynchronous nature of javascript, you should do something like instead. $http.post return a promise object which can be used like following.
return $http.post("/APGame.WebHost/play/newGamePlayed", newGameData);
And in your calling function.
saveNewGame($http, gameData).then(function(response){
gamePlayedId = response.data;
});
I am building routes/states and the menu based on what the user is authorized to see. I've looked around and tried a few different things, but i'm hitting a brick wall. The SessionService object in the RoleService Factory is empty whenever RoleService.validateRole() is called. No route is added and the app is effectively dead. Why is the injected factory empty and the methods undefined.
Here is a simplified layout of the app starting in order of dependencies.
In app.run(), I am adding the states to the app instead of doing it in the config.
$stateProviderRef.state(value.stateName, state);
The states come from (a factory) AppConfig.getStates(), which returns an array.
var states = AppConfig.getStates();
In getStates() we validate each route's role.
if(RoleService.validateRole(routes[i].role))
The RoleService depends on the SessionService and the validateRole function does this check:
if(SessionService.currentUser.role === role)
The SessionService depends on the AuthenticationService which is just a factory that returns a promise using $http (the user object). The SessionService.currentUser is a function that .then()s the returned promise from the AuthenticationService.
return {
currentUser: function(){
AuthenticationService.then(function(result){
return result;
});
}
};
I'm not sure of a better way to explain the code without including the entire files.
Based on the plunker (mentioned in comment), I updated/cloned it to another, which is working
I. simple - when static data are returned (no $http)
Because the service SessonService was defined like this:
return {
currentUser: function() {
...
we cannot call it as a property:
...
return {
validateRoleAdmin: function () {
if (SessionService.currentUser.role === 'admin') {
...
},
validateRole: function (role) {
if(SessionService.currentUser.role === role){
...
it is a function it must be called as a function currentUser():
return {
validateRoleAdmin: function () {
if (SessionService.currentUser().role === 'admin') {
...
},
validateRole: function (role) {
if(SessionService.currentUser().role === role){
...
II. waiting for async calls
The adjusted example
Next, if we in example create a static result of the service AuthenticationService:
angular.module('daedalus').factory('AuthenticationService',
function() {
return {"idsid": "ad_jdschuma","role": "user","id": "33333"}
}
)
we cannot expect there will be some then method:
currentUser: function() {
//AuthenticationService.then(function(result) {
// return result;
//});
return AuthenticationService;
}
And to make it really async we can replace it with this:
angular.module('daedalus').factory('AuthenticationService',
['$timeout', function($timeout) {
return {
getData: function() {
return $timeout(function() {
return {
"idsid": "ad_jdschuma",
"role": "user",
"id": "33333"
}
})
}
};
}])
And then use even the .then() - Session service:
angular.module('daedalus').factory('SessionService', ['AuthenticationService',
function(AuthenticationService) {
return {
currentUser: function(){
return AuthenticationService
.getData()
.then(function(result){
return result;
});
}
};
}]
)
And the RoleService:
return {
...
validateRole: function(route) {
console.log('SessionService currentUser: ' + JSON.stringify(SessionService))
return SessionService
.currentUser()
.then(function(userRole) {
if (userRole.role === route.role) {
return route;
} else {
return null;
}
})
}
And with this in place in appConfig
getStates: function(){
var items = [];
var deffered = $q.defer();
var validatedCount = routes.length;
for(var i=0,len=routes.length; i<len; i++){
var route = routes[i];
RoleService
.validateRole(route)
.then(function(route){
if(route) {
items.push(route.stateConfig)
}
if(--validatedCount === 0 ){ // all processed
deffered.resolve(items)
}
})
}
return deffered.promise;
}
We can do that in run:
AppConfig
.getStates()
.then(function(states) {console.log(states)
angular.forEach(states, function(value, key) {
var state = {
"url": value.url,
"templateUrl": value.templateUrl,
"controller": value.controller
};
$stateProviderRef.state(value.stateName, state);
});
// Configures $urlRouter's listener *after* your custom listener
$urlRouter.sync();
});
$urlRouter.listen();
Check it here
The concept of the second solution (async) is too .thenified(). I just intended to show that all is working. Better approach how to get security data is completely covered here:
Confusing $locationChangeSuccess and $stateChangeStart
have been using a pretty much loving Crockford's constructor, but, having problems adding scoped functions to the object:
'use strict';
var Constructor = function( params ) {
let config = params,
data = params.datum,
action = function(a,b) { return config.actions[a](b); };
return Object.freeze({
action: action
});
};
var cns = Constructor({
datum: 123,
actions: {
getData: function(b) { return data; }
}
});
cns.action('getData',0);
get Uncaught ReferenceError: data is not defined.
how do I have a function as an argument to the constructor and have that function have the scope of object?
If you are following Crockford's private members in JavaScript post, then getData should be a "privileged" function (a function that has access to private members such as data). Therefore, this function should follow the "privileged" pattern given in his post (JSFiddle example).
var Constructor = function (params) {
var config = params;
var data = params.data;
// Privileged function pattern:
// By using a closure, this method has access to private members.
this.getData = function (b) {
return data;
};
};
// Note: Changed to `new` in order to instantiate the class
var cns = new Constructor({
data: 123
});
console.log(cns.getData(0));
the easiest way seems to be to manually pass object guts to the function, either with call or as an extra argument. since I'm dodging this, am using the extra argument, here self. self is not exposed to the world at large, only to the functions that need to see it.
'use strict';
var Constructor = function( params ) {
let config = params,
data = params.datum,
self = { data: data },
action = function(a,b) { return config.actions[a](b,self); };
return Object.freeze({
action: action
});
};
var cns = Constructor({
datum: 123,
actions: {
getData: function(b,s) { return s.data; }
}
});
cns.action('getData',0);