React & React Native Testing Library wait for async state update caused by useEffect on mount - expo

I can't seem to get this simple test to work in react-testing-library & react-native-testing-library. I've tried various combinations of wrapping the render function in act, or using waitFor and other async utils, but the test never waits for the component to re-render after useEffect causes the async api call to set the new state.
Also worth noting I receive the warning: An update to TestComponent inside a test was not wrapped in act(...).`. I'm aware of this issue but no method that I've seen solved it for me.
import React, { useEffect, useState } from 'react'
import { View, Text } from 'react-native'
import { render, waitFor } from 'test-utils'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import { useApi } from './index'
const server = setupServer(
rest.get('http://localhost/MOCK_VAR/some-endpoint', (req, res, ctx) => {
return res(ctx.json({ greeting: 'hello there' }))
})
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
function TestComponent() {
const { apiRequest } = useApi()
const [result, setResult] = useState(null)
useEffect(() => {
makeApiCall()
})
const makeApiCall = async () => {
const apiResult = await apiRequest({ url: '/some-endpoint' })
console.log(apiResult.greeting) // <-- 'hello there'
setResult(apiResult.greeting)
}
return (
<View>
<Text>{result}</Text>
</View>
)
}
describe('Test useApi hook', () => {
test('test post request', async () => {
const { findByText } = render(<TestComponent />)
const greeting = await findByText('hello there')
await waitFor(() => { // <-- never waits
expect(greeting).toBeTruthy()
})
})
})

My issue was awaiting the findBy function. From the docs it says findBy* methods have waitFor already built in. So simply removing the await solved the issue.
What worked for me:
test('test post request', async () => {
const { findByText } = render(<TestComponent />)
const greeting = findByText('hello there')
waitFor(() => expect(greeting).toBeTruthy())
})

Related

React Testing, using axios-mock-adapter

I need to switch out my backend in-memory DB for testing due to memory issues. Below is my code
import { fireEvent, render, screen, waitFor } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
import App from "App";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { AccessLevel, ResponseApi, SystemUserApi } from "types";
let mock: MockAdapter;
beforeAll(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.reset();
});
beforeEach(() => {
jest.resetModules();
});
describe("<App />", () => {
test("login", async () => {
mock.onPost('/Hello').reply(200, getPost);
const result = render(<App />);
const user = userEvent.setup();
const btnLogin = screen.getByText(/Login/i) as HTMLButtonElement;
await userEvent.click(btnLogin);
let btnOk = screen.queryByText(/OK/i) as HTMLButtonElement;
expect(btnOk.disabled).toBe(true);
let btnCancel = screen.getByText(/Cancel/i) as HTMLButtonElement;
expect(btnCancel.disabled).toBe(false);
fireEvent.change(screen.getByLabelText(/Access Code/i) as HTMLInputElement, { target: { value: 'USER' } });
expect(btnOk.disabled).toBe(false);
await userEvent.click(btnOk);
//At this point I was expecting the onPost to be clicked
});
});
function getPost(config: any): any {
console.log(config);
debugger;
return {
data: {
access_code: 'USER'.toUpperCase(),
access_level: AccessLevel.USER ,
lock_level:true
} as SystemUserApi,
error: false,
} as ResponseApi
}
Deep down in the is a call axios post to /Hello but my function within the test is not called. I do not know if it has to do with the actual call being axios.request vs axios.post. I have tried switching to mock.onAny, but that did not seem to work. Not sure what to do here.

How to mock an imported function into a test suite in NestJs?

I want to write a unit test for my payment service but I'm receiving this error:
source.subscribe is not a function
at ./node_modules/rxjs/src/internal/lastValueFrom.ts:60:12
This is my service
import { HttpService } from '#nestjs/axios';
import { Injectable } from '#nestjs/common';
import { lastValueFrom } from 'rxjs';
import { PaymentInfo } from 'src/utils/types/paymentInfo';
#Injectable()
export class PaymentsService {
constructor(private readonly httpService: HttpService) {}
private createHeaderWithAuth(auth, contentType = 'application/json') {
return {
headers: {
authorization: auth.replace('Bearer', '').trim(),
'Content-Type': contentType,
},
};
}
async makePayment(auth: string, paymentInfo: PaymentInfo) {
const configs = this.createHeaderWithAuth(auth);
const response = await lastValueFrom(
await this.httpService.post(
`${process.env.PAYMENT_URL}/transaction/pay`,
paymentInfo,
configs
)
).catch((error) => {
console.log(error);
throw new Error(error.response.data.message);
});
return response.data;
}
}
So with a bit of searching and tinkering found out that this is caused by my import of a rxjs function to resolve the observable setted by axios.
I've searched ways to mock this function so I can properly test my service. But none of them gave me a solution, the questions i found only revolved around functions with modules, but these have none since is imported from a third party lib.
This is my test suite:
describe('Payments Service', () => {
let service: PaymentsService;
let mockedHttpService = {
post: jest
.fn()
.mockImplementation(
async (
url: string,
paymentInfo: PaymentInfo,
header = mockedHeader
) => {
return { mockedSuccessfulResponse };
}
),
get: jest
.fn()
.mockImplementation(async (url: string, header = mockedHeader) => {
return { ...mockedSuccessfulResponse, data: mockedUserCards };
}),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PaymentsService,
{
provide: HttpService,
useValue: mockedHttpService,
},
],
}).compile();
service = module.get<PaymentsService>(PaymentsService);
});
describe('Initialize', () => {
it('should define service', () => {
expect(service).toBeDefined();
});
describe('makePayment', () => {
it('should make a payment', async () => {
const payment = await service.makePayment(mockedAuth, mockedPaymentInfo);
expect(mockedHttpService.post).toHaveBeenCalledWith(
`${process.env.PAYMENT_URL}/transaction/pay`,
mockedPaymentInfo,
mockedHeader
);
expect(payment).toBe(mockedSuccessfulResponse);
});
});
});
Ps.: I removed the mocked objects to reduce the amount of code to read
you should use the of operator from rxjs, and drop the async keyword. Like:
.mockImplementation(
(
url: string,
paymentInfo: PaymentInfo,
header = mockedHeader
) => {
return of({ mockedSuccessfulResponse });
}
otherwise lastValueFrom won't receive an observable object.

Mocking axios in a vue3 typescript unit test using jest and vue-test-utils2 (Solved)

My component calls
this.axios.get()
when being mounted and passes a vuex-store variable to the api. The api returns an array as the response and the component displays some of the returned data after exchanging a loading-element with the real content.
In my unit test I want to simulate the result of the axios-request, wait for the transition between the loading- and the content-element and then finally check the validity of the content. However, the test fails and outputs:
Cannot read property 'get' of undefined
and highlights the get on this.axios.
Here is what I'm expecting to work (based on this guide):
... some imports etc. ...
const mockAxios = { whatIExpectToGet };
jest.mock("axios", () => ({
get: jest.fn(() => mockAxios)
}));
it("description of the test", async () => {
const wrapper = mount(MyComponent);
... code continues ...
Of course I'm accesssing axios via this and not directly like they do in the guide. But, since I can't find any mention of anything related to that, I assume that's irrelevant?
I also tried to mock axios myself like so:
... imports etc. ...
const axios = {
get: Promise.resolve({ whatIExpectToGet })
};
it("description of the test", async () => {
const wrapper = mount(MyComponent, {
global: {
mocks: [ axios ]
}
});
... code continues ...
Apparently people with similar problems used localVue.use() to inject stuff, but that's no longer supported.
Could someone be so kind and smart as to point me into the right direction, please?
Thank you.
-------------------> SOLUTION <-------------------
Thanks to tony 19 this question is already solved.
I ended up using an async function to mock axios because Promise.resolve() wasn't working for me:
import { shallowMount, flushPromises } from "#vue/test-utils";
import MyComponent from "#/components/MyComponent.vue";
describe("MyComponent.vue", () => {
const axios = {
get: async () => ({
data: { expectedData }
})
};
it("test description", async () => {
const wrapper = shallowMount(MyComponent, {
global: {
mocks: {
axios: axios
}
}
} as any);
expect(wrapper.html()).toContain("some_string_i_display_while_loading");
await flushPromises();
expect(wrapper.html()).toContain("some_string_i_display_after_getting_the_response");
});
});
Using global.mocks to mock axios is the right approach, but your attempt incorrectly used an array when it should've been an object:
const wrapper = mount(MyComponent, {
global: {
// mocks: [ axios ] ❌
mocks: { axios } ✅
}
})
Note axios.get() resolves to an axios.Response object, which stores the response data in its data property, so your mock should do the same.
Here's a full example:
// MyComponent.vue
export default {
mounted() {
this.axios.get('foo').then(resp => this.foo = resp.data)
}
}
// MyComponent.spec.js
it('gets foo', () => {
const wrapper = mount(MyComponent, {
global: {
mocks: {
axios: {
get: Promise.resolve({ data: { foo: true }})
// OR use an async function, which internally returns a Promise
get: async () => ({ data: { foo: true }})
}
}
}
}
})

Axios Mock Adapter waiting for timeout before dispatching?

In my React Redux app, I tried to do some tests to one of my function that has settimeout in it
// MyFunc.js
export function updateSomething(id, data) {
return (dispatch, getState) => {
dispatch({type: 'dispatch1'})
settimeout(() => {
axios.get('/data')
.then((res) => {
dispatch({type: 'timeoutted_dispatch', data: res.data})
})
.catch(error => {
dispatch({type: 'dispatch_error'});
})
},3000);
dispatch({type: 'dispatch_end'});
}
}
// MyTest.js
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';;
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('Lets do some tests', () => {
it('Should dispatch timeoutted function', () => {
const store = mockStore({});
// Get all the dispatches for testing purpose
store.dispatch(updateSomething(1, {test: 'test'}))
.then(() => {
const actionList = store.getAction();
expect(actionList).toEqual(allDispatches);
// At this point, I won't be able to get anything inside timeout function
})
});
})
How do I set some kind of Await so I can wait for the timeout before trying to retrieve the data ?

How do you test Collection.allow( ) functions that rely on the user ID?

Given the following collection and access control defintion
class TasksCollection extends Mongo.Collection {
insert (task, callback) {
const doc = _.extend({}, task, {
createdOn: new Date(),
owner: this.userId
})
super.insert(doc, callback)
}
}
export const Tasks = new TasksCollection('tasks')
// Simple checks to ensure that the user is logged in before making changes.
Tasks.allow({
insert: (userId, doc) =>=> !!userId,
update: (userId, doc, fields, modifier) => !!userId,
remove: (userId, doc) => !!userId
})
How would you test to ensure that it works using Mocha/Chai/Sinon? This is what I have tried.
import { Meteor } from 'meteor/meteor'
import { resetDatabase } from 'meteor/xolvio:cleaner';
import { assert, expect } from 'chai'
import { Tasks } from '/imports/api/tasks'
import sinon from 'sinon'
describe('collection test', () => {
beforeEach(() => {
resetDatabase()
})
it('can see a collection', () => {
assert(Tasks, 'unable to see sample collection')
})
it('can query an empty collection', () => {
expect(Tasks.find({}).fetch()).to.be.empty
})
it('fails to add to a collection when the user is not logged in', (done) => {
expect(Tasks.find({}).fetch()).to.be.empty
Tasks.insert({
text: 'hello world'
}, (error) => {
console.log('expected', error) // this is also a 404
assert(error)
done()
})
})
describe('logged in', () => {
let sandbox
beforeEach(() => {
sandbox = sinon.sandbox.create()
sandbox.stub(Meteor, 'userId').returns(42)
})
afterEach(() => {
sandbox.restore()
})
it('can add to a collection', (done) => {
expect(Tasks.find({}).fetch()).to.be.empty
Tasks.insert({
text: 'hello world'
}, (error, _id) => {
console.log(error)
assert(!error)
const results = Tasks.find({}).fetch()
expect(results).to.have.lengthOf(1)
expect(results[0].defaultValue).to.equal(42)
expect(results[0]._id).to.equal(_id)
expect(results[0].createdOn).to.not.be.undefined
done()
})
})
})
})
UPDATE: But I get a 404 error when calling the server.
The insecure package is already removed.
UPDATE: I am only testing on the client for now as the authorization can only be done from a client call.