Mocking a HttpClient with Jest - reactjs

I'm having trouble with my React Native + Jest + Typescript setup.
I'm trying to test a thunk/network operation. I've created a networkClient function:
export const networkClient = async (
apiPath: string,
method = RequestType.GET,
body = {},
authenticate = true,
appState: IAppState,
dispatch: Dispatch<any>
) => {
... validate/renew token, validate request and stuff...
const queryParams = {
method,
headers: authenticate
? helpers.getHeadersWithAuth(tokenToUse)
: helpers.getBaseHeaders(),
body: method === RequestType.POST ? body : undefined,
};
const fullUri = baseURL + apiPath;
const result = await fetch(fullUri, queryParams);
if (result.ok) {
const json = await result.json();
console.log(`Result ${result.status} for request to ${fullUri}`);
return json;
} else {
... handle error codes
}
} catch (error) {
handleNetworkError(error, apiPath);
}
};
Now, when writing my tests for the operations which uses the networkClient above to request server data like so:
const uri = `/subscriptions/media` + tokenParam;
const json = await networkClient(
uri,
RequestType.GET,
undefined,
true,
getState(),
dispatch
);
I'd like to mock the implementation to return a mock response pr. test.
As pr the docs, I thought this could be done like so:
import { RequestType, networkClient} from './path/to/NetworkClient';
and in the test:
networkClient = jest.fn(
(
apiPath: string,
method = RequestType.GET,
body = {},
authenticate = true,
appState: IAppState,
dispatch: Dispatch<any>
) => {
return 'my test json';
}
);
const store = mockStore(initialState);
return store
.dispatch(operations.default.getMoreFeedData(false))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(store.getState().feedData).toEqual(testFeed);
// fetchMock.restore();
});
but networkClient is not defined, and ts tells me
[ts] Cannot assign to 'networkClient' because it is not a variable.
What did I get wrong? I must have missed something about how Jest mocks modules and how to provide a mock implementation somewhere, but I can't find it on neither the docs, nor on Google/SO.
Any help is much appreciated

So I found the solution
The import should not be
import { RequestType, networkClient} from './path/to/NetworkClient';
but instead the module should be required like so:
const network = require('./../../../../networking/NetworkClient');
After that, i could successfully mock the implementation and complete the test:
const testFeed = { items: feed, token: 'nextpage' };
network.networkClient = jest.fn(() => {
return testFeed;
});
I hope it helps someone

Related

All my TRPC queries fail with a 500. What is wrong with my setup?

I am new to TRPC and have set up a custom hook in my NextJS app to make queries. This hook is sending out a query to generateRandomWorker but the response always returns a generic 500 error. I am completely stuck until I can figure out this issue.
The hook:
// filepath: src\utilities\hooks\useCreateRandomWorker.ts
type ReturnType = {
createWorker: () => Promise<Worker>,
isCreating: boolean,
}
const useCreateRandomWorker = (): ReturnType => {
const [isCreating, setIsCreating] = useState(false);
const createWorker = async (): Promise<Worker> => {
setIsCreating(true);
const randomWorker: CreateWorker = await client.generateRandomWorker.query(null);
const createdWorker: Worker = await client.createWorker.mutate(randomWorker);
setIsCreating(false);
return createdWorker;
}
return { createWorker, isCreating };
Here is the router. I know the WorkerService calls work because they are returning the proper values when passed into getServerSideProps which directly calls them. WorkerService.generateRandomWorker is synchronous, the others are async.
// filepath: src\server\routers\WorkerAPI.ts
export const WorkerRouter = router({
generateRandomWorker: procedure
.input(z.null()) // <---- I have tried completely omitting `.input` and with a `null` property
.output(PrismaWorkerCreateInputSchema)
.query(() => WorkerService.generateRandomWorker()),
getAllWorkers: procedure
.input(z.null())
.output(z.array(WorkerSchema))
.query(async () => await WorkerService.getAllWorkers()),
createWorker: procedure
.input(PrismaWorkerCreateInputSchema)
.output(WorkerSchema)
.mutation(async ({ input }) => await WorkerService.createWorker(input)),
});
The Next API listener is at filepath: src\pages\api\trpc\[trpc].ts
When the .input is omitted the request URL is /api/trpc/generateRandomWorker?batch=1&input={"0":{"json":null,"meta":{"values":["undefined"]}}} and returns a 500.
When the .input is z.null() the request URL is /api/trpc/generateRandomWorker?batch=1&input={"0":{"json":null}} and returns a 500.
Can anyone help on what I might be missing?
Additional Info
The client declaration.
// filepath: src\utilities\trpc.ts
export const client = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: `${getBaseUrl() + trpcUrl}`, // "http://localhost:3000/api/trpc"
fetch: async (input, init?) => {
const fetch = getFetch();
return fetch(input, {
...init,
credentials: "include",
})
}
}),
],
transformer: SuperJSON,
});
The init:
// filepath: src\server\trpc.ts
import SuperJSON from "superjson";
import { initTRPC } from "#trpc/server";
export const t = initTRPC.create({
transformer: SuperJSON,
});
export const { router, middleware, procedure, mergeRouters } = t;
Sorry I am not familiar with the vanilla client. But since you're in react you might be interested in some ways you can call a trpc procedure from anywhere while using the react client:
By using the context you can pretty much do anything from anywhere:
const client = trpc.useContext()
const onClick = async () => {
const data = await client.playlist.get.fetch({id})
}
For a known query, you can disable it at declaration and refetch it on demand
const {refetch} = trpc.playlist.get.useQuery({id}, {enabled: false})
const onClick = async () => {
const data = await refetch()
}
If your procedure is a mutation, it's trivial, so maybe you can turn your GET into a POST
const {mutateAsync: getMore} = trpc.playlist.more.useMutation()
const onClick = async () => {
const data = await getMore({id})
}
Answered.
Turns out I was missing the export for the API handler in api/trpc/[trpc].ts

getting issue in jest unit testing for login

i have created login page,which works fine, when i created unit test for that module with jest, i am getting issue, it doesn't return promiss when login api calls, here is my code for react and jest respectively, i am not getting console for console.log("after login"); can anyone please check my code and help to resolve this issue ?
validateAll(formData, rules, message).then(async () => {
dispatch(setLoading(true))
console.log("before login");
const login = await authApi.login(app, email, password)
console.log("after login");
if (login && login.error && login.error !== null) {
dispatch(setLoading(false))
ToastAlert({ msg: login.error.error || login.error.message, msgType: 'error' });
dispatch(setToken(''))
} else {
console.log("done");
dispatch(setToken(login.data._accessToken))
setSuccess(true)
dispatch(setLoading(false))
//router.replace("/");
ToastAlert({ msg: 'You have successfully logged in', msgType: 'success' });
}
}
auth
import { resolve } from "./resolve";
import * as Realm from "realm-web";
function auth() {
const register = async (app: any, data: object) => {
return await resolve(
app.emailPasswordAuth.registerUser(data)
.then((response: any) => response)
)
}
const login = async (app: any, email: string, password: string) => {
const credentials = await Realm.Credentials.emailPassword(email, password);
return await resolve(
app.logIn(credentials)
.then((response: any) => response)
)
}
const confirmUser = async (app: any, data: object) => {
return await resolve(
app.emailPasswordAuth.confirmUser(data)
.then((response: any) => response)
)
}
const logout = async (app: any) => {
return await resolve(
app.currentUser.logOut()
.then((response: any) => response)
)
}
return {
register,
login,
confirmUser,
logout
}
}
const authApi = auth();
export default authApi;
unit test
test('login with correct username and password', async () => {
const initialState = {
email: '',
password: '',
error: {
email: "",
password: ""
},
};
const mockStore = configureStore();
let store;
store = mockStore(initialState);
const { getByText,getByLabelText } = render(
<Provider store={store}>
<Authenticate />
</Provider>
);
// fill out the form
fireEvent.change(screen.getByLabelText('Email'), {
target: {value: '***#gmail.com'},
})
fireEvent.change(screen.getByLabelText(/Parola/i), {
target: {value: '****#123'},
})
const loginAwait = screen.getByText(/Authentificare/i);
await fireEvent.click(loginAwait)
// wait for the error message
const alert = await screen.findByRole('alert')
expect(alert).toHaveTextContent(/You have successfully logged in/i)
})
As mentioned in the comments you can't make real requests in jest. You have 2 big options to solve your issue:
Option 1.1: As was mentioned in the comment (and shown in the link) you can mock your request library (in your case is not axios, is Realm.Credentials.emailPassword, but you probably also have to mock the app.logIn part) :
Just replace Realm, for example, you should add something like this to your unit test:
...
const Realm = require("realm-web");
jest.mock("realm-web")
...
test('login with correct username and password', async () => {
Realm.Credentials.emailPassword.mockResolvedValue({token: "test-token"})
})
WARNING: As mentioned above this by itself most likely won't fix your problem since you have to also mock app.logIn (however assuming app.logIn is just calling under the hood Realm.App.logIn you might be able to mock that too by adding:
Realm.App.logIn.mockResolvedValue({user: {...})
(Realm.App might need to be Realm.App())
If Realm.Credentials.emailPassword throws an error you might need to define them first when defining jest.mock("realm-web"). So something like :
jest.mock("realm-web", () => ({
App: (config) => {
return {
logIn: jest.fn()
}
},
Credentials: {
emailPassword: jest.fn()
}
}))
or you can just mock the library at the beginning using something like:
jest.mock("realm-web", () => ({
App: (config) => {
return {
logIn: (token:string) => ({user: {...}})
}
},
Credentials: {
emailPassword: (email: string, password:string) => ({token: "test-token"})
}
}))
)
If this is not the case you need to figure how to mock that as well (is kind of hard to properly fix your issue without a working example). But assuming you are doing something like app = new Realm.App(...) you might want to check how to mock new Function() with Jest here. If you get to this, you will most likely need a hybrid solution (to mock both new Realm.App and Realm.Credentials.emailPassword)
You could also try to mock the entire module at once, at the beginning of the test file using something like:
jest.mock("realm-web", () => ({
App: (config) => {
return {
logIn: (token:string) => ({user: {...}})
}
},
Credentials: {
emailPassword: (email: string, password:string) => ({token: "test-token"})
}
}))
OBS: adjusments might be required. This is just an example.
Also please be aware that this will create a mock for all the tests following the execution of this code.
Option 1.2:
You could also use a similar strategy to mock your authApi (you have some examples on how to mock a default export here). Just make sure you mock the the login function from it (and if there is any other function from the api -> like confirmUser used on the same test mock that as well). This option would be easier to implement but it's really up to what you want your test to cover. Also a hybrid might be mocking the app part together with Realm.Credentials.emailPassword.
Option 2: You might find an existing solution. Here are some interesting links:
github Realm mock
react native example from mogoDB page -> this won't work copy-pasted, but might serve as inspiration.
Another maybe somehow related question would be How to fake Realm Results for tests. It is not answering your question but might also help a little.

How to mock Axios service wrapper using JEST

I'm using JEST testing framework to write test cases for my React JS application. I'm using our internal axios wrapper to make service call. I would like to mock that wrapper service using JEST. Can someone help me on this ?
import Client from 'service-library/dist/client';
import urls from './urls';
import { NODE_ENV, API_VERSION } from '../screens/constants';
const versionHeader = 'X-API-VERSION';
class ViewServiceClass extends Client {
getFiltersList(params) {
const config = {
method: urls.View.getFilters.requestType,
url: urls.View.getFilters.path(),
params,
headers: { [versionHeader]: API_VERSION },
};
return this.client.request(config);
}
const options = { environment: NODE_ENV };
const ViewService = new ViewServiceClass(options);
export default ViewService;
Above is the Service Implementation to make API call. Which I'm leveraging that axios implementation from our internal library.
getFiltersData = () => {
const params = {
filters: 'x,y,z',
};
let {
abc,
def,
ghi
} = this.state;
trackPromise(
ViewService.getFiltersList(params)
.then((result) => {
if (result.status === 200 && result.data) {
const filtersJson = result.data;
.catch(() =>
this.setState({
alertMessage: 'No Filters Data Found. Please try after some time',
severity: 'error',
showAlert: true,
})
)
);
};
I'm using the ViewService to get the response, and I would like to mock this service. Can someone help me on this ?
You would need to spy your getFiltersList method from ViewServiceClass class.
Then mocking some response data (a Promise), something like:
import ViewService from '..';
const mockedData = {
status: 'ok',
data: ['some-data']
};
const mockedFn = jest.fn(() => Promise.resolve(mockedData));
let getFiltersListSpy;
// spy the method and set the mocked data before all tests execution
beforeAll(() => {
getFiltersListSpy = jest.spyOn(ViewService, 'getFiltersList');
getFiltersListSpy.mockReturnValue(mockedFn);
});
// clear the mock the method after all tests execution
afterAll(() => {
getFiltersListSpy.mockClear();
});
// call your method, should be returning same content as `mockedData` const
test('init', async () => {
const response = await ViewService.getFiltersList();
expect(response).toEqual(mockedData);
});
P.D: You can pass params to the method, but you will need to configure as well the mockedData as you wish.

Axios Mock Adapter with repeated params

I am am using a mock adapter for my tests in a react app, and one of them looks like this:
http://endpoint.com/api/entities?id=123&select=labels&select=versions&labelsLang=en
The important part to note is that the select parameter is in there twice.
One of the tests is rendering in another language, so we have two mock endpoints set up to reflect this, however I cannot seem to find a way to properly mock the repeated paramter. I just keep on getting a result for the first one.
The code for the mocked endpoints is this:
const mockApiClient = axios.create({ baseURL: "http://localhost" });
const mock = new MockAdapter(mockApiClient);
const params1 = new URLSearchParams();
params1.append("id", "123");
params1.append("select", "labels");
params1.append("select", "versions");
params1.set("labelsLang", "en");
mock
.onGet("/entities", {
asymmetricMatch: function(actual: any) {
return actual.params.toString() === params1.toString();
},
})
.reply(200, getCompanyResponse);
const params2 = new URLSearchParams();
params2.append("id", "123");
params2.append("select", "labels");
params2.append("select", "versions");
params2.set("labelsLang", "de");
mock
.onGet("/entities", {
asymmetricMatch: function(actual: any) {
return actual.params.toString() === params2.toString();
},
})
.reply(200, getCompanyResponseDE);
I know it's messy, I just want to understand how to do this properly.
Whenever I try specifying specific params in an object, it complains that you cant have a duplicate key .
(ie.
{ params:{select:'labels', select:'versions} })
Solved.
Here's how it was done:
const mockApiClient = axios.create({ baseURL: "http://localhost" });
const mock = new MockAdapter(mockApiClient);
const params1 = {
"id": "123",
select: ["labels", "versions"],
"labelsLang": "en",
};
mock
.onGet("/entities", {
params: {
asymmetricMatch: function(actual: any) {
actual.sort(); // Important, as without it the order of params would affect the result
return actual.toString() === toURLSearchParams(params1).toString();
},
},
})
.reply(200, getCompanyResponse);
export const toURLSearchParams = (params: {[key: string]: string|string[]}, sort:boolean = true):URLSearchParams => {
if(params instanceof URLSearchParams) return params
const searchParams = new URLSearchParams();
for(const key in params){
const value = params[key];
if(Array.isArray(value)){
value.forEach((eachValue) => {
searchParams.append(key, eachValue)
})
} else {
searchParams.append(key,value)
}
}
if(sort) searchParams.sort() // Likewise here.
return searchParams;
}

How to test custom async/await hook with react-hooks-testing-library

I created a custom react hook that is supposed to handle all less important api requests, which i don't want to store in the redux state. Hook works fine but I have trouble testing it. My test setup is jest and enzyme, but I decided to give a try react-hooks-testing-library here as well.
What I have tried so far is to first mock fetch request with a fetch-mock library, what works fine. Next, i render hook with renderHook method, which comes from react-hooks-testing-library. Unfortunately, looks like I do not quite understand the waitForNextUpdate method.
This is how my hook looks like.
useApi hook
export function useApi<R, B = undefined>(
path: string,
body: B | undefined = undefined,
method: HttpMethod = HttpMethod.GET
): ResponseStatus<R> {
const [response, setResponse] = useState();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | boolean>(false);
useEffect(() => {
const fetchData = async (): Promise<void> => {
setError(false);
setIsLoading(true);
try {
const result = await callApi(method, path, body);
setResponse(result);
} catch (errorResponse) {
setError(errorResponse);
}
setIsLoading(false);
};
fetchData();
}, [path, body, method]);
return { response, isLoading, error };
}
Hook can take 3 different state combinations that I would like to test. Unfortunately, I have no idea how.
Loading data:
{ response: undefined, isLoading: true, error: false }
Loaded data:
{ response: R, isLoading: false, error: false }
Error:
{ response: undefined, isLoading: false, error: true }
This is how my test looks like at this moment:
import fetchMock from 'fetch-mock';
import { useApi } from './hooks';
import { renderHook } from '#testing-library/react-hooks';
test('', async () => {
fetchMock.mock('*', {
returnedData: 'foo'
});
const { result, waitForNextUpdate } = renderHook(() => useApi('/data-owners'));
console.log(result.current);
await waitForNextUpdate();
console.log(result.current);
});
callApi func
/**
* Method to call api.
*
* #param {HttpMethod} method - Request type.
* #param {string} path - Restful endpoint route.
* #param {any} body - Request body data.
*/
export const callApi = async (method: HttpMethod, path: string, body: any = null) => {
// Sends api request
const response = await sendRequest(method, path, body);
// Checks response and parse to the object
const resJson = response ? await response.json() : '';
if (resJson.error) {
// If message contains error, it will throw new Error with code passed in status property (e.g. 401)
throw new Error(resJson.status);
} else {
// Else returns response
return resJson;
}
};
It's all right that you did. Now, you need use expect for test.
const value = {...}
expect(result.current).toEqual(value)

Resources