Jasmine, React & AJAX: Unit testing function within a function - javascript

Here's the code I would like to test. Specifically, I want to spy on a utility called Linkvalidation.validate to make sure that it is called when handleSave() is called.
This code lives in a component called the CondensedFormModal:
handleSave() {
LinkValidation.validate(this.state.url)
.then((response) => {
if (response.success) {
this.setState({
message: ''
});
}
})
.fail((error) => {
if (error && error.message && error.message.match(/invalid internal link/)) {
this.setState({
message: 'The URL is an internal link. Please use a public link.'
});
} else {
this.setState({
message: 'The URL is invalid.'
});
}
});
Here is the LinkValidation.validate utility I'm using in the handleSave function above:
define([
'module/api-call'
], function(
ApiCall
) {
'use strict';
// Calls validation API
function validate(link) {
var deferred = $.Deferred();
ApiCall.apiCall(
'/url/check',
{ link: link },
'POST',
function(data) {
if (data.success === true) {
deferred.resolve(data);
} else {
// This link is not valid
deferred.reject(data);
}
},
function() {
deferred.reject();
}
);
return deferred;
}
return {
validate: validate
};
});
Here is my test file--
Import statement:
import { validate } from 'modules/link-validation.js';
Test:
describe('when the URL is an internal link', () => {
it('displays a unique error message', (done) => {
let modal = shallowInstance(<CondensedFormModal />);
modal.setState({
url: 'https://internal.example.com'
});
let x = jasmine.createSpy('validate').and.returnValue({
message: "invalid internal link",
success: false,
url: 'https://example.com'
});
modal.handleSave();
_.defer(() => {
expect(x).toHaveBeenCalled();
done();
});
});
});
When I run this test, it consistently fails with the message "Expected spy validate to have been called."
After looking at the Jasmine docs (https://jasmine.github.io/2.1/introduction) and various other Stack Overflow questions (Unit test with spy is failing. Says spy was never called , Jasmine test case error 'Spy to have been called' , etc.) I'm unable to make this work. I've also tried callFake and callThrough instead of returnValue.
Any ideas on how to spy on LinkValidation.validate to assure that it was called?

This line:
let x = jasmine.createSpy('validate')
creates new spy function (it doesn't spy on existing validate function) and handleSave function is not aware of it. So it's not called at all.
You have to set spy on function that is actually called in your component. Since your CondensedFormModal uses LinkValidation module (which I assume is imported in component file) you have to set spy on validate function from imported module which is actually used by component. So I'd suggest something like this:
In CondensedFormModal constructor set LinkValidation as component property to make it easily accessible in tests:
this.LinkValidation = LinkValidation;
In handleSave use validate function like this:
this.LinkValidation.validate(this.state.url);
And finally in test set spy on component validate method:
describe('when the URL is an internal link', () => {
it('displays a unique error message', (done) => {
let modal = shallowInstance(<CondensedFormModal />);
...
spyOn(modal.LinkValidation, 'validate').and.returnValue({
message: "invalid internal link",
success: false,
url: 'https://dash.vagrant.local.rf29.net/shopping/new'
});
modal.handleSave();
_.defer(() => {
expect(modal.LinkValidation.validate).toHaveBeenCalled();
done();
});
});
});

Related

Jasmine AJAX spy failure

Ok these tests were passing a few bit ago. I have made no changes to which version of jasmine I'm using but... can anyone see obvious syntax errors here?
describe("ajax return", function() {
beforeEach(function() {
ajaxSpy = spyOn($, "ajax")
})
describe("on success", function() {
beforeEach(async function() {
ajaxSpy.and.callFake(function(e) {
e.success({"success":true, "remove":{1:"test"},"assign_prestock":"test2"})
})
await catalogDOM.syncAvailability(null)
})
it("should call", function() {
...
})
})
})
When running, I'm getting this error:
1_catalogDOM_spec.js:518 Uncaught (in promise) TypeError: e.success is not a function
UPDATE code for catalogDOM.syncAvailability
catalogDOM.syncAvailability: function(item_name_id) {
return new Promise(function(resolve, reject) {
$.ajax({
type:"POST",
url: "/retrieve-options-availability",
dataType:"json",
contentType:"application/json",
data: JSON.stringify(params)
})
.done(function(response, status_string, jqxhr) {
if (response["success"] == true) {
resolve()
} else {
reject(response["message"])
}
})
.fail(function(jqxhr, error_string, exception_object){
reject("Error loading availability. Refresh & try again or contact us if this persists.")
})
}
Try doing this to debug:
ajaxSpy.and.callFake(function(e) {
// add console.log here !!
console.log('e: ', e);
e.success({"success":true, "remove":{1:"test"},"assign_prestock":"test2"})
})
Apparently, .success is not a function anymore and you can look at the value of e there. I am thinking e is the argument for what's provided in $.ajax(/* e is here */);.
Looking at the documentation here: https://api.jquery.com/jquery.ajax/, I think we need to mock a done function.
Try this:
ajaxSpy.and.callFake(function (e) {
return {
done: function () {
return {
"success":true,
"remove":{1:"test"},
"assign_prestock":"test2",
// edit - create a fail function that's empty here
fail: function() {
// leave empty
}
};
}
};
});
Edit
Instead of doing a spy on ajaxSpy, try spying on catalogDOM directly. Something like this:
spyOn(catalogDOM, 'syncAvailability').and.resolveTo({/* Mock value here */ });
Or
spyOn(catalogDOM, 'syncAvailability').and.returnValue(Promise.resolve({ /* Mock value here */ });
And then you don't have to await it.

Matcher error: received value must be a mock or spy function

I'm writing tests (with Jest and React Testing Library) for a form React component. I have a method that runs on form submit:
const onSubmit = (data) => {
// ...
setIsPopupActive(true);
// ...
};
and useEffect that runs after isPopupActive change, so also on submit:
useEffect(() => {
if (isPopupActive) {
setTimeout(() => {
setIsPopupActive(false);
}, 3000);
}
}, [isPopupActive]);
In the test, I want to check, whether the popup disappears after 3 seconds. So here's my test:
it('Closes popup after 3 seconds', async () => {
const nameInput = screen.getByPlaceholderText('Imię');
const emailInput = screen.getByPlaceholderText('Email');
const messageInput = screen.getByPlaceholderText('Wiadomość');
const submitButton = screen.getByText('Wyślij');
jest.useFakeTimers();
fireEvent.change(nameInput, { target: { value: 'Test name' } });
fireEvent.change(emailInput, { target: { value: 'test#test.com' } });
fireEvent.change(messageInput, { target: { value: 'Test message' } });
fireEvent.click(submitButton);
const popup = await waitFor(() =>
screen.getByText(/Wiadomość została wysłana/)
);
await waitFor(() => {
expect(popup).not.toBeInTheDocument(); // this passes
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 3000);
});
});
However, I'm getting the error:
expect(received).toHaveBeenCalledTimes(expected)
Matcher error: received value must be a mock or spy function
Received has type: function
Received has value: [Function setTimeout]
What am I doing wrong?
Jest 27 has breaking changes for fakeTimers. It seems Jest contributors doesn't update documentation on time. This comment on Github issues confirms it. Moreover, here related PR.
Well, you can solve your problem by two ways.
Configure Jest to use legacy fake timers. In jest.config.js you can add line (but it not works for me):
module.exports = {
// many of lines omited
timers: 'legacy'
};
Configure legacy fake timers for individually test suite, or even test:
jest.useFakeTimers('legacy');
describe('My awesome logic', () => {
// blah blah blah
});
It's preferably to use new syntax based on #sinonjs/fake-timers. But I can't find working example for Jest, so I'll update this answer as soon as possible.
The below approach worked
beforeEach(() => {
jest.spyOn(global, 'setTimeout');
});
afterEach(() => {
global.setTimeout.mockRestore();
});
it('Test if SetTimeout is been called', {
global.setTimeout.mockImplementation((callback) => callback());
expect(global.setTimeout).toBeCalledWith(expect.any(Function), 7500);
})
In your case setTimeout is not a mock or spy, rather, it's a real function. To make it a spy, use const timeoutSpy = jest.spyOn(window, 'setTimeout'). And use timeoutSpy in the assertion.
You could also test not the fact of calling the setTimeout function, but assert that setIsPopupActive was called once, and with false. For this you might need to do jest.runOnlyPendingTimers() or jest.runAllTimers()

Vue: Component method from vuex module?

I'm using namespaced modules for state management in vuex, I try to keep all my actions mutations inside my modules as this helps me keep most of my code in the same place (modules acting like classes or similar) hoever there's an issue, I'd like to fire a component method to clear a form when a vuex action is successfull (that is the axios request gets an OK/200 response) but sadly I can't fire a methos from vuex module into my component ( there's no this inisde module).
I also tried adding a .then to my action call but it fires right after I call the action...
I guess I could move the action into the component itself but I'd rather not, what do you guys suggest?
My component:
stripeSourceHandler: function(sourceId)
{
if(this.customerSources.length == 0)
{
console.log('createSourceAndCustomer');
this.createSourceAndCustomer({ id: sourceId });
}
else
{
console.log('addSource');
this.addSource({ id: sourceId }).then(alert('Form cleared')); //this fires right after calling addSource
};
},
My module action:
addSource({ commit }, sourceId)
{
commit('Loader/SET_LOADER', { status:1, message: 'Procesando...' }, { root: true });
axios.post('/stripe/add-source', sourceId)
.then((response) => {
commit('Loader/SET_LOADER', { status:2, message: response.data.message }, { root: true });
commit('ADD_SOURCE', response.data.customer);
//I can't clear component form from this...
},
(error) => {
commit('Loader/SET_LOADER', { status:3, errors: error, message: 'Error al añadir el pago.' }, { root: true });
});
},
Two issues:
You need to return the promise from the action so that you can use .then() to schedule code to be executed once the action has completed (this code being whatever you need to do to clear the form).
.then() takes one (or two) functions as parameters which will be called once the promise has resolved, instead you're just calling alert() immediately.
It'll be something like:
Component method
stripeSourceHandler(sourceId) {
if (this.customerSources.length == 0) {
this.createSourceAndCustomer({ id: sourceId });
} else {
this.addSource({ id: sourceId }).then(() => {
// Clear the form here
alert('Form cleared');
});
}
}
Vuex action
addSource({ commit }, sourceId) {
commit('Loader/SET_LOADER', { status:1, message: 'Procesando...' }, { root: true });
// Return the promise here
return axios.post('/stripe/add-source', sourceId)
.then(response => {
commit('Loader/SET_LOADER', { status:2, message: response.data.message }, { root: true });
commit('ADD_SOURCE', response.data.customer);
}, error => {
commit('Loader/SET_LOADER', { status:3, errors: error, message: 'Error al añadir el pago.' }, { root: true });
});
}

ignored children tests in Frisby JS with jasmine-node

I use Frisy with jasmine-node to test an Meteor API.
I want to test the remove of a discussion in a chat App. For this, I need to create a new discussion in chat and add a message in the discussion.
I noticed that my test fail if I put it after the second .then() method. It fails also after the third .then(). However, it works correctly after the first .then() method.
An example code with explicit failed test expect(false).toBe(true);:
var frisby = require('frisby');
describe("chat update", function() {
it("message added", function(done) {
frisby.post('http://localhost:3000/api/chat', {
name: "remove"
})
.then(function (res) {
let id = res._body.id;
expect(false).toBe(true); // (1) it fails the test
frisby.post('http://localhost:3000/api/chat/'+id+'/addmessage',
{
auteur: "Samuel",
message: "My message"
}
)
.then(function (res) {
expect(false).toBe(true); // (2) Without (1) it's ignored by frisby
frisby.post('http://localhost:3000/api/chat/remove',
{id: id}
)
.then(function (res) {
expect(false).toBe(true); // (3) Without (1) it's ignored by frisby
})
});
})
.done(done);
})
});
If I run the test, it will fail thanks to expect(false).toBe(true); // (1) it fails the test line.
If I remove this line, the test will run and jasmine valid it as right.
Do you know a way to don't ignore the (2) and (3) tests ?
Finaly, I found the solution.
It's because I forgot to return all frisby action (except the first one) like in the following code:
var frisby = require('frisby');
describe("chat update", function() {
it("message added", function(done) {
frisby.post('http://localhost:3000/api/chat', {
name: "remove"
})
.then(function (res) {
let id = res._body.id;
return frisby.post('http://localhost:3000/api/chat/'+id+'/addmessage',
{
auteur: "Samuel",
message: "My message"
}
)
.then(function (res) {
return frisby.post('http://localhost:3000/api/chat/remove',
{id: id}
)
.then(function (res) {
expect(false).toBe(true); // Will fail the test
})
});
})
.done(done);
})
});
You may notice the return operators before frisby.post().
I hope it will help !

Why my test failed after adding didInsertElement to fetch json in ember?

I have a simple component that it's going to fetch data after the component is inserted. It was ok until I run my test. I got this error.
Assertion Failed: You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in a run
I understand that the component is fetching data asynchronously but I'm not sure how to solve that in my integration test.
Here's my code
export default Ember.Component.extend({
didInsertElement: function() {
let source = this.get('source');
let url = apiUrl + source;
Ember.$.getJSON(url).then(function(response) {
this.set('data', response.data);
}.bind(this));
},
// something else
};
And this is my test.
moduleForComponent('panel', 'Integration | Component | panel', {
integration: true,
beforeEach () {
this.render(hbs`{{panel}}`);
}
});
test('it has source dropdown', function(assert) {
assert.equal(this.$('select[name="Source"]').length, 1);
});
Without the fetching data bit, the test runs ok.
Try wrapping the getJSON inside an Ember run loop.
So something like this:
var self = this;
Ember.run(function(){
Ember.$.getJSON(url).then(function(response) {
self.set('data', response.data);
});
});
Create a new Promise and then set the response data in the success handler.
The promise returned by Em.$.getJSON and new Ember.RSVP.Promise are different.
let self = this;
let promise = new Ember.RSVP.Promise(function() {
return Ember.$.getJSON(url);
});
promise.then((json) => {
this.set('data', json);
});

Categories