I am currently working on unit test of a VueJS project, and I would like to create a my-component.spec.js test (with Jest) for a my-component.vue component that uses a myService service with the inject option. The test itself is not important. It's above all about learning how to properly mock a component that uses a service. After doing a lot of research, I tried to write my test that I think is on the right track, but I can't seem to get it to work completely. Here is the code with 3 console.log in the test file :
mock.js (file containing a simulated method of the myService service) :
export default function getAllInfos() {
return [
{"id": "myId", "name": "myName"}
];
}
my-component.spec.js :
import {mount} from "#vue/test-utils";
import MyComponent from "./my-component.vue";
import getAllInfos from "./mock";
describe("MyComponent", () => {
it("returns the correct value for getAllInfos() method", () => {
const wrapper = mount(MyComponent, {
"provide": {
myService() {
return {
"mock": {
"getAllInfos": getAllInfos()
}
};
},
},
data() {
return {
"infos": []
};
}
});
console.log("getAllInfos = ", wrapper.vm.$options.provide.myService().mock.getAllInfos);
const test = wrapper.vm.$options.provide.myService().mock.getAllInfos;
console.log("test = ", test);
wrapper.setData({"infos": test});
console.log("infos = ", wrapper.vm.$options.data().infos);
expect(wrapper.vm.$options.data().infos).toEqual([
{"id": "myId", "name": "myName"}
]);
});
});
I think I'm on the right track because when I run the npm run test:unit command, the first 2 console.log return the expected result :
getAllInfos = [{id: 'myId', name: 'myName'}]
test = [{id: 'myId', name: 'myName'}]
However, the 3rd console.log seems strange to me :
infos = []
This means the infos data is not assigned by the test constant, the content of which is correct.
Anyone have a solution for this problem ?
If you take a closer look at the docs (setData | Vue Test Utils), then you can see a small but significant difference to your code:
// your code:
/* ... */
it("returns the correct value for getAllInfos() method", () => {
/* ... */
wrapper.setData({"infos": test});
/* ... */
});
/* ... */
// code in docs:
/* ... */
test('setData demo', async () => {
/* ... */
await wrapper.setData({ foo: 'bar' })
/* ... */
})
Yes, the difference is that in the docs, the test is an async function and the setData is awaited. I think this is the source of your problem.
You could also try (but this is a bit of a workaround):
import { createLocalVue, mount } from "#vue/test-utils";
/* ... */
const localVue = createLocalVue()
/* ... */
wrapper.setData({"infos": test});
await localVue.nextTick()
/* ... */
Source for localVue here
Related
If I have the following js file how would I test the getTextValue and getRadioValue functions using Jest:
function getTextValue(textArea) {
return document.getElementById(textArea).value;
}
function getRadioValue(radioGroup) {
.....
return returnedValue;
}
export default class alpha {
constructor() {
...
}
myMethod() {
const answers = {
answers: [
{ id: "1", text: getRadioValue("a")},
{ id: "2", text: getTextValue("b")}
]
};
}
}
I'd like my test to be something like:
action: myMethod is called,
expect: getTextValue toHaveReturnedWith(Element)
You should make a test file for exemple alphaTest.spec.js in your test folder:
import alpha from '/yourfolder'
describe('Alpha', () => {
test('getValue should have returned element', () => {
// act
alpha.myMethod()
// assert
expect(getTextValue).toHaveReturnedWith("b")
})
}
You don't have document context in the node environment.
You have to use the library which emulates browser functionality and populates all the necessary functions you have like document.getElementById etc.
One option is to use JSDOM and create testing HTML snippet for it.
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const textareaValue = 'My-value';
const dom = new JSDOM(`
<!DOCTYPE html>
<body>
<textarea id="textarea" value="${textareaValue}"></textarea>
</body>
`);
const { document } = dom;
function getTextValue(textArea) {
return document.getElementById(textArea).value;
}
export default class Alpha {
myMethod() {
return { id: 1, text: getTextValue("textarea")};
}
}
describe('Alpha', () => {
it('should parse textare value', () => {
const alpha = new Alpha();
expect(alpha.myMethod()).toEqual({
id: 1,
text: textareaValue
});
});
});
If you want to test complex logic and interaction with different Browser APIs better to use
Cypress, Selenium, etc. These things run actual browsers while testing but are not really recommended for just unit tests. So, option one, to use JSDOM is recommended for your case.
I need to use jest.mock together with async, await and import() so that I can use a function from another file inside the mocked module. Otherwise I must copy and paste a few hundreds of slocs or over 1000 slocs, or probably it is not even possible.
An example
This does work well:
jest.mock('./myLin.jsx', () => {
return {
abc: 967,
}
});
Everywhere I use abc later it has 967 as its value, which is different than the original one.
This does not work:
jest.mock('./myLin.jsx', async () => {
return {
abc: 967,
}
});
abc seems to not be available.
Actual issue
I need async to be able to do this:
jest.mock('~/config', async () => {
const { blockTagDeserializer } = await import(
'../editor/deserialize' // or 'volto-slate/editor/deserialize'
);
// … here return an object which contains a call to
// blockTagDeserializer declared above; if I can't do this
// I cannot use blockTagDeserializer since it is outside of
// the scope of this function
}
Actual results
I get errors like:
TypeError: Cannot destructure property 'slate' of '((cov_1viq84mfum.s[13]++) , _config.settings)' as it is undefined.
where _config, I think, is the ~/config module object and slate is a property that should be available on _config.settings.
Expected results
No error, blockTagDeserializer works in the mocked module and the unit test is passed.
The unit test code
The code below is a newer not-working code based on this file on GitHub.
import React from 'react';
import renderer from 'react-test-renderer';
import WysiwygWidget from './WysiwygWidget';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-intl-redux';
const mockStore = configureStore();
global.__SERVER__ = true; // eslint-disable-line no-underscore-dangle
global.__CLIENT__ = false; // eslint-disable-line no-underscore-dangle
jest.mock('~/config', async () => {
const { blockTagDeserializer } = await import(
'../editor/deserialize' // or 'volto-slate/editor/deserialize'
);
const createEmptyParagraph = () => {
return {
type: 'p',
children: [{ text: '' }],
};
};
return {
settings: {
supportedLanguages: [],
slate: {
elements: {
default: ({ attributes, children }) => (
<p {...attributes}>{children}</p>
),
strong: ({ children }) => {
return <strong>{children}</strong>;
},
},
leafs: {},
defaultBlockType: 'p',
textblockExtensions: [],
extensions: [
(editor) => {
editor.htmlTagsToSlate = {
STRONG: blockTagDeserializer('strong'),
};
return editor;
},
],
defaultValue: () => {
return [createEmptyParagraph()];
},
},
},
};
});
window.getSelection = () => ({});
test('renders a WysiwygWidget component', () => {
const store = mockStore({
intl: {
locale: 'en',
messages: {},
},
});
const component = renderer.create(
<Provider store={store}>
<WysiwygWidget
id="qwertyu"
title="My Widget"
description="My little description."
required={true}
value={{ data: 'abc <strong>def</strong>' }}
onChange={(id, data) => {
// console.log('changed', data.data);
// setHtml(data.data);
}}
/>
</Provider>,
);
const json = component.toJSON();
expect(json).toMatchSnapshot();
});
What I've tried
The code snippets above show partially what I have tried.
I searched the web for 'jest mock async await import' and did not found something relevant.
The question
If jest.mock is not made to work with async, what else can I do to make my unit test work?
Update 1
In the last snippet of code above, the line
STRONG: blockTagDeserializer('strong'),
uses blockTagDeserializer defined here which uses deserializeChildren, createEmptyParagraph (which is imported from another module), normalizeBlockNodes (which is imported from another module) and jsx (which is imported from another module) functions, which use deserialize which uses isWhitespace which is imported from another module and typeDeserialize which uses jsx and deserializeChildren.
Without using await import(...) syntax how can I fully mock the module so that my unit test works?
If you want to dig into our code, please note that the volto-slate/ prefix in the import statements is for the src/ folder in the repo.
Thank you.
I'd advise not doing any "heavy" stuff (whatever that means) in a callback of jest.mock, – it is designed only for mocking values.
Given your specific example, I'd just put whatever the output of blockTagDeserializer('strong') right inside the config:
jest.mock('~/config', () => {
// ...
extensions: [
(editor) => {
editor.htmlTagsToSlate = {
STRONG: ({ children }) => <strong>{children}</strong>, // or whatever this function actually returns for 'strong'
};
return editor;
},
],
// ...
});
This doesn't require anything asynchronous to be done.
If you need this setup to be present in a lot of files, extracting it in a setup file seems to be the next best thing.
I found a solution. I have a ref callback that sets the htmlTagsToSlate property of the editor in the actual code of the module, conditioned by global.__JEST__ which is defined as true in Jest command line usage:
import { htmlTagsToSlate } from 'volto-slate/editor/config';
[...]
testingEditorRef={(val) => {
ref.current = val;
if (val && global.__JEST__) {
val.htmlTagsToSlate = { ...htmlTagsToSlate };
}
}}
Now the jest.mock call for ~/config is simple, there is no need to do an import in it.
I also use this function:
const handleEditorRef = (editor, ref) => {
if (typeof ref === 'function') {
ref(editor);
} else if (typeof ref === 'object') {
ref.current = editor;
}
return editor;
};
I'm using jest and trying to Mock the ora package's spinner.start to see if it was called. I was trying the following as I think they have done it in another post here
My code (lala.js):
import ora from 'ora';
const spinner = ora({ indent: 2 });
export const lala = () => {
spinner.start('dfsdfsdf');
return 'hey';
};
export default lala();
test file:
import { lala } from './lala';
import ora from 'ora';
jest.mock('ora', () => () => {
const start = jest.fn();
const result = { start };
return result;
});
describe('lala', () => {
it.only('calls start', () => {
lala();
const spinner = ora();
expect(spinner.start).toHaveBeenCalled();
});
});
The error i'm getting from Jest is that it wasn't called as show in the following code block:
expect(jest.fn()).toHaveBeenCalled()
Expected number of calls: >= 1
Received number of calls: 0
I have tried so many things and importing/mocking this many ways but can't seem to get it to work. Eventually, I want to do toHaveBeenCalledWith('dfsdfsdf') but I can't even mock this correctly yet. Any help would be appreciated. I am using the following versions of Jest and Ora:
"jest": "24.8.0",
"ora": "3.2.0",
Thanks in advance.
I managed to get a working solution. I solved this by creating a helper function which initialises the ora object and is used in lala.js.
import ora from 'ora';
export const spinner = ora({ indent: 2 });
that way I can import the object in my test file and spy on start and then use the spy in my expectation like so:
import { spinner } from './utils/ora-helper';
export const lala = () => {
spinner.start('dfsdfsdf');
return 'hey';
};
export default lala();
import { lala } from './lala';
import { spinner } from './utils/ora-helper';
describe('lala', () => {
it.only('calls start', () => {
const spinnerSpy = jest.spyOn(spinner, 'start');
lala();
expect(spinnerSpy).toHaveBeenCalled(); // works like a charm
});
});
This is the structure of my project (create-react-app):
Contents of /src/api/searchAPI.js:
import client from './client';
async function searchMulti(query, options = {}) {
options.query = query;
return await client.get('/search/multi', options);
}
export default {
searchMulti
};
Contents of /src/api/index.js:
import movieAPI from './movieAPI';
import personAPI from './personAPI';
import searchAPI from './searchAPI';
import configurationAPI from './configurationAPI';
export { movieAPI, personAPI, searchAPI, configurationAPI };
QuickSearch component imports searchAPI ands uses it to fetch some data over the web.
Now, I need to test (with react-testing-library) the QuickSearch component.
So, I would like to mock the api module (exported in /src/api/index.js) in order to use a mock function instead of searchAPI.searchMulti( ).
If I put below code in /src/componentns/__tests__/QuickSearch.js, it works just fine:
...
import { searchAPI } from '../../
...
...
jest.mock('../../api', () => {
return {
searchAPI: {
searchMulti: jest.fn().mockResolvedValue({ results: [] })
}
};
});
...
it('some test', () => {
searchAPI.searchMulti.mockResolvedValueOnce({ results: [] });
const { queryByTitle, getByPlaceholderText } = renderWithRouter(
<QuickSearch />
);
const input = getByPlaceholderText(/Search for a movie or person/i);
expect(searchAPI.searchMulti).not.toHaveBeenCalled();
act(() => {
fireEvent.change(input, { target: { value: 'Aladdin' } });
});
expect(searchAPI.searchMulti).toHaveBeenCalledTimes(1);
});
My problem is that I don't want to mock api in every test file that needs it. Instead, I would like to put api in a __mocks__ folder so that other tests can use it you, too.
How can I do that?
I'm having a difficult time trying to test a methods in meteor that requires a connected user. Basically I need to test if a user of the app can add an article to it's cart. The methods will tests if a user is connected and, in order to test that will use Meteor.userId(). This seems to be a problem in unit testing as I get the error:
"Meteor.userId can only be invoked in method calls or publications."
So far, I tried to do what's proposed in this post: How to unit test a meteor method with practicalmeteor:mocha but I don't understand what the solution is doing.
Here is my testing method:
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { assert } from 'meteor/practicalmeteor:chai';
import { sinon } from 'meteor/practicalmeteor:sinon';
import { Carts } from '/imports/api/carts/carts.js';
import { Articles } from '/imports/api/articles/articles.js';
import '/imports/api/carts/carts.methods.js';
import { SecurityEnsurer } from '/lib/security/security.js';
function randomInt (low, high) {
return Math.floor(Math.random() * (high - low) + low);
}
if (Meteor.isServer) {
describe('Carts', () => {
describe('methods', () => {
let currentUser;
beforeEach(() => {
Factory.define('user', Meteor.users, {
name: "userTest",
currentUser: {
email: 'user#shop.info',
password: '123456',
}
});
currentUser = Factory.create('user');
sinon.stub(Meteor, 'user');
Meteor.user.returns(currentUser);
Articles.remove({});
articleId = Articles.insert({
name : "azerty",
description : "descTest",
modelNumber : "wxcvbn",
categoryName : "CatTest",
price : 1,
advisedPrice: 2,
supplierDiscount : 0,
brandId : "BrandTest",
isAvailable: true,
restockingTime: 42,
color: "Yellow",
technicals: [
{
name : "GPU",
value : "Intel"
},
],
});
Carts.insert({
owner: currentUser,
entries: [],
});
});
afterEach(() => {
Meteor.user.restore();
Articles.remove({});
Carts.remove({});
});
it('can add article', () => {
let quantity = randomInt(1,50);
const addArticleToCart = Meteor.server.method_handlers['carts.addArticle'];
const invocation = {};
addArticleToCart.apply(invocation, [articleId, quantity]);
assert.equal(Cart.find({owner: currentUser, entries: {$elemMatch: {articleId, quantity}}}).count(), 1);
});
});
});
}
If anyone can help me find out how to create my test, this would realy help me.
To fake a user when calling a Meteor Method, the only way I found is to use the mdg:validated-method package which provide a framework around Meteor methods. This framework seems to be the standard now (see the Meteor guide), but it requires to re-write your methods and the in-app calls.
After describing the methods using this framework, you are able to call them with the userId parameter when testing, using this kind of code (which verifies that my method is returning a 403 error):
assert.throws(function () {
updateData._execute({userId: myExternalUserId}, {
id: dataId,
data: {name: "test"}
});
}, Meteor.Error, /403/);
FYI, here are the packages I add when I do automated testing (Meteor 1.6 used):
meteortesting:mocha
dburles:factory
practicalmeteor:chai
johanbrook:publication-collector
Here's how I set up a fake logged in user for testing publish and methods:
1) create a user
2) stub i.e. replace the Meteor.user() and Meteor.userId() functions which return the current logged in user in methods
3) provide that user's _id to PublicationsCollector, which will send it in to your publish function.
Here's how I did it, I hope you can adapt from this:
import { Meteor } from 'meteor/meteor';
import { Factory } from 'meteor/dburles:factory';
import { PublicationCollector } from 'meteor/johanbrook:publication-collector';
import { resetDatabase } from 'meteor/xolvio:cleaner';
import faker from 'faker';
import { Random } from 'meteor/random';
import { chai, assert } from 'meteor/practicalmeteor:chai';
import sinon from 'sinon';
// and also import your publish and collection
Factory.define('user', Meteor.users, {
'name': 'Josephine',
});
if (Meteor.isServer) {
describe('Menus', () => {
beforeEach(function () {
resetDatabase();
const currentUser = Factory.create('user');
sinon.stub(Meteor, 'user');
Meteor.user.returns(currentUser); // now Meteor.user() will return the user we just created
sinon.stub(Meteor, 'userId');
Meteor.userId.returns(currentUser._id); // needed in methods
// and create a Menu object in the Menus collection
});
afterEach(() => {
Meteor.user.restore();
resetDatabase();
});
describe('publish', () => {
it('can view menus', (done) => {
const collector = new PublicationCollector({ 'userId': Meteor.user()._id }); // give publish a value for this.userId
collector.collect(
'menus',
(collections) => {
assert.equal(collections.menus.length, 1);
done();
},
);
});
});
});
}
You can also write a test for calling a Meteor method that relies on Meteor.userId():
expect(() => { Meteor.call('myMethod'); }).to.not.throw(Meteor.Error);