I want to start writing unit tests for my code. I thought I'd start with a nice and simple function.
string => string
in action here: http://jsbin.com/yufuzawalo/edit?js,console,output
const animEndString = (type = 'transition') => {
let types =
type === 'transition'
? {
OTransition: 'oTransitionEnd',
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
transition: 'transitionend'
}
: {
OAnimation: 'oAnimationEnd',
WebkitAnimation: 'webkitAnimationEnd',
MozAnimation: 'animationend',
animation: 'animationend'
}
const elem = document.createElement('div')
return Object.keys(types).reduce(function(prev, trans) {
return undefined !== elem.style[trans] ? types[trans] : prev
}, '')
}
And the test:
describe('example', () => {
it('should return animationend when passed the string animation', () => {
const value = animationEnd('animation')
expect(value).toBe('animationend')
})
it('should return transitionEnd when passed the string animation', () => {
const value = animationEnd('transition')
expect(value).toBe('transitionend')
})
})
output:
example › should return transitionEnd when passed the string animation
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"transitionend"
Received:
The test is failing as an empty string is being returned. I'm presuming that Jest doesn't know what to do with document.createElement('fake')
How would I get around this issue?
Solved...
I've added the following to my setup-jest file
global.document.createElement = jest.fn(() => ({
style: {
transition: 'opacity 300ms ease',
animation: 'test 300ms'
}
}))
Related
I was struggling with a test issue for my custom useLazyQuery hook. My first test is passing but the second one is failing. What am doing wrong for the second test?
Here is the useLazyFetchCoin.tsx
export const useLazyFetchCoin = () => {
const [coins, setCoins] = useState<ICoin[]>([]);
useEffect(() => {
const coins = localStorage.getItem('coinsInfo');
if (coins) {
setCoins(JSON.parse(coins));
}
}, []);
const [getData, { loading, error }] = useLazyQuery(GET_COIN_PRICE_QUERY, {
fetchPolicy: 'network-only',
notifyOnNetworkStatusChange: true,
onCompleted: (data) => {
const hasSameCoin = coins.some((f) => f.id === data.markets[0]?.id);
if (data.markets.length && !hasSameCoin) {
const allCoins = [...coins, data.markets[0]];
setCoins(allCoins);
localStorage.setItem('coinsInfo', JSON.stringify(allCoins));
} else if (data.markets.length <= 0) {
alertNotification('Coin not found !', Notification.ERROR);
}
if (hasSameCoin) {
alertNotification('This coin already exists on your list !', Notification.WARNING);
}
}
});
return { coins, setCoins, getData, loading, error };
};
Here is the test file
describe('useLazyFetchCoin custom hook', () => {
const QueryMock = [
{
request: {
query: GET_COIN_PRICE_QUERY,
variables: { code: 'BNB' }
},
result: {
data: {
markets: [
{
id: 'binance_bnb_eur',
baseSymbol: 'BNB',
ticker: {
lastPrice: '414.90000000'
}
}
]
}
}
}
];
const QueryWrongCodeMock = [
{
request: {
query: GET_COIN_PRICE_QUERY,
variables: { code: 'asd' }
},
result: {
data: {
markets: []
}
}
}
];
function getHookWrapper(mocks: any, code: string) {
const wrapper = ({ children }: any) => (
<MockedProvider mocks={mocks} addTypename={false}>
{children}
</MockedProvider>
);
const { result, waitForNextUpdate } = renderHook(() => useLazyFetchCoin(), {
wrapper
});
expect(typeof result.current.coins).toBe('object');
expect(result.current.loading).toBeFalsy();
expect(result.current.error).toBeUndefined();
// call the lazy function
act(() => {
result.current.getData({
variables: { code }
});
});
return { result, waitForNextUpdate };
}
it('should return an array of coins', async () => {
// Working correctly
const { result, waitForNextUpdate } = getHookWrapper(QueryMock, 'BNB');
await waitForNextUpdate();
expect(result.current.loading).toBeFalsy();
expect(result.current.coins[0]).toEqual({
id: 'binance_bnb_eur',
baseSymbol: 'BNB',
ticker: {
lastPrice: '414.90000000'
}
});
});
it('should return an empty array when requesting a wrong code', async () => {
// Not working
const { result, waitForNextUpdate } = getHookWrapper(QueryWrongCodeMock, 'asd');
await waitForNextUpdate();
expect(result.current.loading).toBeFalsy();
expect(result.current.coins[0]).toEqual([]);
});
});
I got this error message for the second test.
Expected: []
Received: {"baseSymbol": "BNB", "id": "binance_bnb_eur", "ticker": {"lastPrice": "414.90000000"}}
I don't get it because I'm using different queries for each test.
Also, the second test should receive an empty array when you pass a wrong code such as 'asd'.
How can write a proper test for it?
I fixed the problem. When I was changing the order test, It worked correctly.
I added a clear mock function for it.
clear mock
I've been trying to mock
admin.firestore.FieldValue.serverTimestamp().
However once I've
mocked admin.firestore(), all the calls just hit that stub instead.
So the error runnning the test is... Trying to stub property 'serverTimestamp' of undefined
const admin = require("firebase-admin")
const test = require("firebase-functions-test")()
describe("Unit test", () => {
let allFunctions, adminFirestoreStub
beforeAll(() => {
adminFirestoreStub = sinon.stub(admin, "initializeApp")
allFunctions = require("../index")
})
describe("Main", () => {
it("Test", async () => {
const dbStub = sinon.stub()
const collStub = sinon.stub()
sinon.stub(admin, "firestore").get(() => dbStub)
dbStub.returns({
collection: collStub,
})
sinon.stub(admin.firestore.FieldValue, "serverTimestamp").returns(Date())
const wrapped = test.wrap(allFunctions.my_func)
await wrapped(data, context)
})
})
})
Thanks Doug, put a const to serverTimestamp() above the actual function and it worked.
Using Vue CLI I have a unit test that I am trying to check for a true/false that looks like this:
describe('The thing', () => {
it('must be available.', () => {
const available = true
const wrapper = shallowMount(MyVueComponent, {
propsData: { available },
})
expect(wrapper).to.be.true
})
})
When I run npm run test:unit
I get the following:
AssertionError: expected { Object (isFunctionalComponent, _emitted, ...) } to be true
If I just check the value of available, then it's all good. But that seems like I'm doing it wrong.
Other tests I have written are working fine as I am checking for a text value:
describe('The thing', () => {
it('should have a name.', () => {
const name = 'Hal'
const wrapper = shallowMount(MyVueComponent, {
propsData: { name },
})
expect(wrapper.text()).to.include(name)
})
})
I am not sure how to check that the available is a boolean and it must be true. Any suggestions would be appreciated!
EDIT
This is what my Vue component looks like:
export default {
name: 'MyVueComponent',
props: {
name: String
},
data: function() {
return {
available: true,
}
},
}
EDIT 2
This seems to work in my unit test:
describe('The thing', () => {
it('must be available.', () => {
const available = true
const wrapper = shallowMount(MyVueComponent, {
propsData: { available },
})
expect(wrapper.vm.available).to.be.true
})
})
However, it is looking at my actual component in my /src directory. If I change the data values from true to false my tests come out correctly. I'm not sure how to have the data stay at the test level. So if I were to change const available = false, my test should fail--but it does not.
EDIT 3
It seems like this works (to access the data object):
describe("The thing", () => {
it("must be available.", () => {
const defaultData = MyVueComponent.data();
// const wrapper = shallowMount(MyVueComponent, {});
expect(defaultData.available).to.be.true;
});
});
But it still seems not right that I'm referencing my actual code, and not sticking within the unit tests.
You want to check the received prop, which you can do with wrapper.props()
describe('The thing', () => {
it('must be available.', () => {
const available = true
const wrapper = shallowMount(MyVueComponent, {
propsData: { available },
})
expect(wrapper.props().available).to.be.true
// or:
// expect(wrapper.props().available).to.equal(available)
})
})
Chai's .to.be.true and .to.equal use === so there is no need to separately check that it is indeed a Boolean, but if you prefer the "expressiveness" of it, you can check it too:
expect(wrapper.props().available).to.be.a('boolean')
What I am looking to do:
spy on the method calls chained onto find() used in a static Model method definition
chained methods: sort(), limit(), skip()
Sample call
goal: to spy on the arguments passed to each of the methods in a static Model method definition:
... static method def
const results = await this.find({}).sort({}).limit().skip();
... static method def
what did find() receive as args: completed with findSpy
what did sort() receive as args: incomplete
what did limit() receive as args: incomplete
what did skip() receive as args: incomplete
What I have tried:
the mockingoose library but it is limited to just find()
I have been able to successfully mock the find() method itself but not the chained calls that come after it
const findSpy = jest.spyOn(models.ModelName, 'find');
researching for mocking chained method calls without success
I was not able to find a solution anywhere. Here is how I ended up solving this. YMMV and if you know of a better way please let me know!
To give some context this is part of a REST implementation of the Medium.com API I am working on as a side project.
How I mocked them
I had each chained method mocked and designed to return the Model mock object itself so that it could access the next method in the chain.
The last method in the chain (skip) was designed to return the result.
In the tests themselves I used the Jest mockImplementation() method to design its behavior for each test
All of these could then be spied on using expect(StoryMock.chainedMethod).toBeCalled[With]()
const StoryMock = {
getLatestStories, // to be tested
addPagination: jest.fn(), // already tested, can mock
find: jest.fn(() => StoryMock),
sort: jest.fn(() => StoryMock),
limit: jest.fn(() => StoryMock),
skip: jest.fn(() => []),
};
Static method definition to be tested
/**
* Gets the latest published stories
* - uses limit, currentPage pagination
* - sorted by descending order of publish date
* #param {object} paginationQuery pagination query string params
* #param {number} paginationQuery.limit [10] pagination limit
* #param {number} paginationQuery.currentPage [0] pagination current page
* #returns {object} { stories, pagination } paginated output using Story.addPagination
*/
async function getLatestStories(paginationQuery) {
const { limit = 10, currentPage = 0 } = paginationQuery;
// limit to max of 20 results per page
const limitBy = Math.min(limit, 20);
const skipBy = limitBy * currentPage;
const latestStories = await this
.find({ published: true, parent: null }) // only published stories
.sort({ publishedAt: -1 }) // publish date descending
.limit(limitBy)
.skip(skipBy);
const stories = await Promise.all(latestStories.map(story => story.toResponseShape()));
return this.addPagination({ output: { stories }, limit: limitBy, currentPage });
}
Full Jest tests to see implementation of the mock
const { mocks } = require('../../../../test-utils');
const { getLatestStories } = require('../story-static-queries');
const StoryMock = {
getLatestStories, // to be tested
addPagination: jest.fn(), // already tested, can mock
find: jest.fn(() => StoryMock),
sort: jest.fn(() => StoryMock),
limit: jest.fn(() => StoryMock),
skip: jest.fn(() => []),
};
const storyInstanceMock = (options) => Object.assign(
mocks.storyMock({ ...options }),
{ toResponseShape() { return this; } }, // already tested, can mock
);
describe('Story static query methods', () => {
describe('getLatestStories(): gets the latest published stories', () => {
const stories = Array(20).fill().map(() => storyInstanceMock({}));
describe('no query pagination params: uses default values for limit and currentPage', () => {
const defaultLimit = 10;
const defaultCurrentPage = 0;
const expectedStories = stories.slice(0, defaultLimit);
// define the return value at end of query chain
StoryMock.skip.mockImplementation(() => expectedStories);
// spy on the Story instance toResponseShape() to ensure it is called
const storyToResponseShapeSpy = jest.spyOn(stories[0], 'toResponseShape');
beforeAll(() => StoryMock.getLatestStories({}));
afterAll(() => jest.clearAllMocks());
test('calls find() for only published stories: { published: true, parent: null }', () => {
expect(StoryMock.find).toHaveBeenCalledWith({ published: true, parent: null });
});
test('calls sort() to sort in descending publishedAt order: { publishedAt: -1 }', () => {
expect(StoryMock.sort).toHaveBeenCalledWith({ publishedAt: -1 });
});
test(`calls limit() using default limit: ${defaultLimit}`, () => {
expect(StoryMock.limit).toHaveBeenCalledWith(defaultLimit);
});
test(`calls skip() using <default limit * default currentPage>: ${defaultLimit * defaultCurrentPage}`, () => {
expect(StoryMock.skip).toHaveBeenCalledWith(defaultLimit * defaultCurrentPage);
});
test('calls toResponseShape() on each Story instance found', () => {
expect(storyToResponseShapeSpy).toHaveBeenCalled();
});
test(`calls static addPagination() method with the first ${defaultLimit} stories result: { output: { stories }, limit: ${defaultLimit}, currentPage: ${defaultCurrentPage} }`, () => {
expect(StoryMock.addPagination).toHaveBeenCalledWith({
output: { stories: expectedStories },
limit: defaultLimit,
currentPage: defaultCurrentPage,
});
});
});
describe('with query pagination params', () => {
afterEach(() => jest.clearAllMocks());
test('executes the previously tested behavior using query param values: { limit: 5, currentPage: 2 }', async () => {
const limit = 5;
const currentPage = 2;
const storyToResponseShapeSpy = jest.spyOn(stories[0], 'toResponseShape');
const expectedStories = stories.slice(0, limit);
StoryMock.skip.mockImplementation(() => expectedStories);
await StoryMock.getLatestStories({ limit, currentPage });
expect(StoryMock.find).toHaveBeenCalledWith({ published: true, parent: null });
expect(StoryMock.sort).toHaveBeenCalledWith({ publishedAt: -1 });
expect(StoryMock.limit).toHaveBeenCalledWith(limit);
expect(StoryMock.skip).toHaveBeenCalledWith(limit * currentPage);
expect(storyToResponseShapeSpy).toHaveBeenCalled();
expect(StoryMock.addPagination).toHaveBeenCalledWith({
limit,
currentPage,
output: { stories: expectedStories },
});
});
test('limit value of 500 passed: enforces maximum value of 20 instead', async () => {
const limit = 500;
const maxLimit = 20;
const currentPage = 2;
StoryMock.skip.mockImplementation(() => stories.slice(0, maxLimit));
await StoryMock.getLatestStories({ limit, currentPage });
expect(StoryMock.limit).toHaveBeenCalledWith(maxLimit);
expect(StoryMock.addPagination).toHaveBeenCalledWith({
limit: maxLimit,
currentPage,
output: { stories: stories.slice(0, maxLimit) },
});
});
});
});
});
jest.spyOn(Post, "find").mockImplementationOnce(() => ({
sort: () => ({
limit: () => [{
id: '613712f7b7025984b080cea9',
text: 'Sample text'
}],
}),
}));
Here is how I did this with sinonjs for the call:
await MyMongooseSchema.find(q).skip(n).limit(m)
It might give you clues to do this with Jest:
sinon.stub(MyMongooseSchema, 'find').returns(
{
skip: (n) => {
return {
limit: (m) => {
return new Promise((
resolve, reject) => {
resolve(searchResults);
});
}
}
}
});
sinon.stub(MyMongooseSchema, 'count').resolves(searchResults.length);
This worked for me:
jest.mock("../../models", () => ({
Action: {
find: jest.fn(),
},
}));
Action.find.mockReturnValueOnce({
readConcern: jest.fn().mockResolvedValueOnce([
{ name: "Action Name" },
]),
});
All the above didn't work in my case, after some trial and error this worked for me:
const findSpy = jest.spyOn(tdataModel.find().sort({ _id: 1 }).skip(0).populate('fields'), 'limit')
NOTE: you need to mock the query, in my case I use NestJs:
I did the following:
find: jest.fn().mockImplementation(() => ({
sort: jest.fn().mockImplementation((...args) => ({
skip: jest.fn().mockImplementation((...arg) => ({
populate: jest.fn().mockImplementation((...arg) => ({
limit: jest.fn().mockImplementation((...arg) => telemetryDataStub),
})),
})),
})),
})),
findOne: jest.fn(),
updateOne: jest.fn(),
deleteOne: jest.fn(),
create: jest.fn(),
count: jest.fn().mockImplementation(() => AllTelemetryDataStub.length),
for me it worked like this:
AnyModel.find = jest.fn().mockImplementationOnce(() => ({
limit: jest.fn().mockImplementationOnce(() => ({
sort: jest.fn().mockResolvedValue(mock)
}))
}))
I am working on a React Native component handling gestures via PanResponder.
I would like to test that when certain dx, dy values are passed to the onPanResponderRelease method, expected action will be executed. Please find below an example of what I would like to test:
export default class MyComponent extends Component<Props, State> {
constructor(props: Props) {
super(props);
this._panResponder = this._initPanResponder();
}
_initPanResponder(): PanResponder {
const panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([
null,
{ dx: this._animation.x, dy: this._animation.y }
]),
onPanResponderRelease: (e, gestureState) => {
const { dx, dy } = gestureState;
if (dy > 100) {
doSomething(); // <---- what I would like to unit test
}
}
});
return panResponder;
}
}
Is there any straightforward way to unit test the following with Jest?
I tend to leave testing the wiring code out, since testing an actual PanResponder involves a UI test that is hard to write, is usually flaky, and gives little value.
What you can do, is extract the event handling function out, and test it independently.
In that case, the test would very simple, as all you need to do is invoke the handler in the test and see you're getting what you expected.
If you want to test user's interection with the component you should mock PanResponder in a way you forward the functions given as params to PanResponder.create to the panHandlers it returns.
// setup-tests.ts
jest.doMock('react-native', () => {
return Object.setPrototypeOf(
PanResponder: {
...ReactNative.PanResponder,
create: (config: any) => ({ panHandlers: config }),
},
},
ReactNative
);
});
Tha way, when yoou spread panHandlers in the View you are handling Pan, you can access the functions the way you set up in the file
// pan-handling-component.tsx
const Component: React.FC = () => {
...
const panResponder = React.useRef(
PanResponder.create({
...
onPanResponderMove: (_, gestureState) => {
Animated.event([slideAnim], { useNativeDriver: false })(
gestureState.moveY
);
},
onPanResponderTerminationRequest: () => true,
onPanResponderRelease: () => {
// #ts-ignore
const currentValue = slideAnim._value;
if (currentValue < 0) {
slideIn();
} else if (currentValue > height - 300) {
handleClose();
}
},
})
).current;
...
return (
<View
testID={DRAWER_WRAPPER_TESTID}
{...panResponder.panHandlers}
>
...
</View>
);
And, finally, the unit test would be something like this
// pan-handling-component.test.tsx
...
const panHandler = instance.root.findByProps({
testID: DRAWER_WRAPPER_TESTID,
});
act(() => {
panHandler.props.onPanResponderMove(eventMock, gestureStateMOck);
panHandler.props.onPanResponderRelease();
});
...