Testing a component in Next.js with testing-library that relies on tRCP - reactjs

I was experimenting with tRCP and diligently followed the setup for my Next.js project described in the official docs over here: https://trpc.io/docs/nextjs
However I noticed that a simple component that relies on tRPC such as this
export const Sample = () => {
const { data } = trpc.useQuery(['hello', { text: 'User' }]);
if (data === undefined) {
return <div>Loading...</div>;
}
return <div>{data.greeting}</div>;
};
cannot be properly tested since the following trivial test
describe('Sample', () => {
it('should render successfully', () => {
const { baseElement } = render(<Sample />);
expect(baseElement).toBeTruthy();
});
});
since there is no setup of provider such as the setup with the withTRCP HOC used for the application itself. As such the test fails claiming client (presumably the trcpClient, unlike the queryClient) is undefined.
I'd like to know how to setup the test correctly, in this case providing a correct client, as well as mocking the queries, since I don't have the respective server-side code running while invoking the tests.

Since you are getting undefined for the trpc client implementation, you can try spying on the query call.
import trpc from 'utils/trpc'; // This is the client implementation
describe('Sample', () => {
it('should render successfully', () => {
jest.spyOn(trpc, 'useQuery')
.mockReturnValue({ greeting: "Greeting" });
const { baseElement } = render(<Sample />);
expect(baseElement).toBeTruthy();
});
});
This is also possible with the mutations but you need to provide a mock implementation for the useMutation response for mutate property.

Related

Is it possible to mock functions outside of the test file for multiple tests to use?

Thanks in advance
Issue
I have some functions that need to be mocked in every test file. However, only in some of those files, do I actually care to evaluate or test those mock functions.
For example, let's say, that when my app renders, it immediately fetches some data In A.test.tsx I want to mock that fetch and verify that it was called.
But, in B.test.tsx, I still need to mock that fetch, but I don't care to verify that it was called.
Goal
I would like the setup to look something like this:
setup.ts
import * as SomeAPI from 'api';
const setup = () => {
jest.spyOn(SomeAPI, 'fetchData');
return { SomeAPI };
};
export default setup;
A.test.tsx
const { SomeAPI } = setup();
beforeEach(() => {
jest.clearAllMocks();
});
test('data should be fetched when app renders', async () => {
RenderWithProviders(<App />);
expect(SomeAPI.fetchData).toHaveBeenCalled();
});
B.test.tsx
setup(); // Don't destructure because I don't care to test the mock
beforeEach(() => {
jest.clearAllMocks();
});
test('test some other stuff', async () => {
// In this case, the fetchData still needs to be mocked<br>
// because the rest of the app depends on it
RenderWithProviders(<App />);
expect(someElement).toBeInTheDocument();
});
My Current Problem
My problem is that while I'm trying to attempt this way of returning mocked functions... If that mocked function is used more than once in the same test file, the jest.clearAllMocks() seems not to have an effect. I assume because the setup is happening outside of the test?
Is this possible to setup mocks outside of the test and only destructure them when needed?

Cypress: Not able to stub with a basic example. What might i be missing?

For some reason, I am not able to stub this here. I have reduced my code to almost exactly this here. This should work according to the docs, but I'm wondering if I'm missing a finer detail of typescript / react hooks? That doesn't feel at all like the case but who knows. Thanks ahead of time if you're reading this and taking up your time. I appreciate what you do. Here's my example:
// connection.ts (the method to stub)
export const connectFn = async () => {
return 'i should not be here'
}
// ./reactHook.ts
import { connectFn } from './connection'
export const useMyHook = () => {
const handleConnect = async () => {
const result = await connectFn()
console.log(result)
// expected: 'i have been stubbed!'
// actual: 'i should not be here
}
return {
handleConnect
}
}
// ./App.ts
export const App = () => {
const { handleConnect } = useMyHook()
return <button onClick={handleConnect}>Connect</button>
}
// ./cypress/integration/connection.spec.ts
import * as myConnectModule from '../../connection'
describe('stub test', () => {
it('stubs the connectFn', () => {
cy.stub(myConnectModule, 'connectFn').resolves('i have been stubbed!')
cy.get('[data-testid=connect-btn]').click()
// assertions about the view here...
})
})
I thought I understood the cypress and Sinon docs pretty well. I have read that cypress needs an object to a stub from and not the named export directly - hence the * as an import. I have also tried every which way to export it as well. I used their example from the docs directly to no avail. I then thought it may be a typescript issue but it doesn't seem to be the case. I have reduced my actual code to pretty much this example here. I have even removed everything but the logs for testing and I'm still not getting it.
To stub successfully you need to stub (replace) the same instance.
In reactHook.ts
import { connectFn } from './connection'
if (window.Cypress) {
window.connectFn = connectFn
}
In the test
cy.window().then(win => {
cy.stub(win, 'connectFn').resolves('i have been stubbed!')
cy.get('[data-testid=connect-btn]').click()
...
}}
BTW useMyHook implies a React hook, if so you may need a cy.wait(0) to release the main JS thread and allow the hook to run.

Jest to test a class method which has inner function

I'm writing unit test for once of my .ts file. Where I'm facing a problem and unable to find the solution. Hopefully someone can help me to resolve it.
Problem
While writing unit test. I'm unable to test the value for profile. After calling a method called getProfile().
File setup
Profile.ts
import { getProfileAPI} from "./api";
class ProfileDetails implements IProfileDetails {
public profile: string = ''
constructor() {}
getProfile = async () => {
const { data } = await getProfileAPI();
if (data) {
this.profile = data
}
};
}
const profileDetail = new ProfileDetails();
export default profileDetail;
Profile.spec.ts
import Profile from './Profile';
describe('Profile', () => {
it('getProfile', async () => {
Profile.getProfile = jest.fn();
await Profile.getProfile();
expect(Profile.getProfile).toHaveBeenCalled();
});
});
So the challenge I'm facing here is, I can able to mock the getProfile method. But I'm not able to mock the getProfileAPI function which is called inside the getProfile method.
How can I mock a function which is called inside a mocked method (or) is there any other way to resolve this. Kindly help.
Thanks in advance.
Before answering your questions, I may have some comments :
your test is wrong, all it does is calling the method then checking if it is called, of course it will always pass !
you are not really mocking, in fact you're erasing the old method and it may have some impacts on other tests.
your method "getProfile" should be called "getAndSetProfile", or "syncProfile", or something like that, getProfile is confusing for a developer, he will think it only get the profile and returns it.
I don't recommend creating & exporting an instance of ProfileDetails like this, you should take a look on DI (Dependency Injection) with typedi for example.
Do not forget :
A unit test means that any dependency inside your "unit" should be mock, you must only test the logic inside your "unit" (in your case, the getProfile function, or the class itself).
Here, you are invoking a method called "getProfileAPI" from another service that is not mocked, so you are currently testing its logic too.
This test should work :
Profile.spec.ts
jest.mock('./api', () => ({
getProfileAPI: jest.fn(),
}));
import { getProfileAPI } from "./api";
import Profile from './Profile';
describe('Profile', () => {
it('getProfile', async () => {
await Profile.getProfile();
expect(getProfileAPI).toHaveBeenCalled();
});
});
In our example, Profile.profile will be empty, because even if we mocked to getProfileAPI method, we didn't make it return something. You could test both cases :
jest.mock('./api', () => ({
getProfileAPI: jest.fn(),
}));
import { getProfileAPI } from "./api";
import Profile from './Profile';
const mockGetProfileAPI = getProfileAPI as jest.Mock; // Typescript fix for mocks, else mockResolvedValue method will show an error
describe('Profile', () => {
describe('getProfile', () => {
describe('with data', () => {
const profile = 'TEST_PROFILE';
beforeEach(() => {
mockGetProfileAPI.mockResolvedValue({
data: profile,
});
});
it('should call getProfileAPI method', async () => {
await Profile.getProfile();
expect(mockGetProfileAPI).toHaveBeenCalled(); // Please note that "expect(getProfileAPI).toHaveBeenCalled();" would work
});
it('should set profile', async () => {
await Profile.getProfile();
expect(Profile.profile).toBe(profile);
});
});
describe.skip('with no data', () => {
it('should not set profile', async () => {
await Profile.getProfile();
expect(Profile.profile).toStrictEqual(''); // the default value
});
});
});
});
NB : I skipped the last test because it won't work in your case. Profile isn't recreated between tests, and as it is an object, it keeps the value of Profile.profile (btw, this is a bit weird) between each tests. This is one of the reasons why you should not export a new instance of the class.

Jest - how to mock a class in jest

I've been trying to mock a test in jest through the methods that they have on their documentation. By mocking the whole class but I can't seem to get it to work properly.
https://jestjs.io/docs/en/es6-class-mocks
jest.mock('../../../../../src/SubscriptionOrder');
SubscriptionOrder.prototype.createChargebeeSubscription = jest.fn(() => 'response');
const test = new SubscriptionOrder(
'subscription',
[{}],
'errorMethods',
'customerMethods',
);
test.createChargebeeSubscription();
I'd expect this to mock the createChargebeeSubscription method and return the string response but it seems to be returning undeifined
Then this is the piece of code I'm trying to run a test for as well.
const subscriptionOrder = new SubscriptionOrder(
'subscription',
subscriptionRequest,
errorMethods,
customerMethods,
);
const response = await subscriptionOrder.createChargebeeSubscription(token);
this.setState({ successfulSubmit: response });
I want to update the state to the string response but getting undefined instead. so it appears I'm kinda mocking something but just not properly.
You can use spyOn as follows to do the mocking for you. I also recommend that you set up and tear down this spy once you are finished.
So here's a sample piece of code which will do what you want:
describe('createChargebeeSubscription() method behaviour', () => {
let createChargebeeSubscriptionSpy;
let testResponse;
beforeAll(() => {
// Lets create an instance of your class first
const subscriptionOrder = new SubscriptionOrder(
'subscription',
subscriptionRequest,
errorMethods,
customerMethods
);
// Now use a spy to mock the return value from your function
createChargebeeSubscriptionSpy = jest.spyOn(subscriptionOrder, 'createChargebeeSubscription').mockImplementation(() => {
return 'response';
});
// Finally invoke the method being tested
testResponse = subscriptionOrder.createChargebeeSubscription();
});
afterAll(() => {
// Restore the functionality (ie. disable the spy) of your method
createChargebeeSubscriptionSpy.mockRestore();
});
it('verifies that the expected response was returned', () => {
expect(testResponse).toBe('response');
});
});

Return value of a mocked function does not have `then` property

I have the following async call in one of my React components:
onSubmit = (data) => {
this.props.startAddPost(data)
.then(() => {
this.props.history.push('/');
});
};
The goal here is to redirect the user to the index page only once the post has been persisted in Redux (startAddPost is an async action generator that sends the data to an external API using axios and dispatches another action that will save the new post in Redux store; the whole thing is returned, so that I can chain a then call to it in the component itself). It works in the app just fine, but I'm having trouble testing it.
import React from 'react';
import { shallow } from 'enzyme';
import { AddPost } from '../../components/AddPost';
import posts from '../fixtures/posts';
let startAddPost, history, wrapper;
beforeEach(() => {
startAddPost = jest.fn();
history = { push: jest.fn() };
wrapper = shallow(<AddPost startAddPost={startAddPost} history={history} />);
});
test('handles the onSubmit call correctly', () => {
wrapper.find('PostForm').prop('onSubmit')(posts[0]);
expect(startAddPost).toHaveBeenLastCalledWith(posts[0]);
expect(history.push).toHaveBeenLastCalledWith('/');
});
So I obviously need this test to pass, but it fails with the following output:
● handles the onSubmit call correctly
TypeError: Cannot read property 'then' of undefined
at AddPost._this.onSubmit (src/components/AddPost.js:9:37)
at Object.<anonymous> (src/tests/components/AddPost.test.js:25:46)
at process._tickCallback (internal/process/next_tick.js:109:7)
So how can I fix this? I suspect this is a problem with the test itself because everything works well in the actual app. Thank you!
Your code is not testable in the first place. You pass in a callback to the action and execute it after saving the data to the database like so,
export function createPost(values, callback) {
const request = axios.post('http://localhost:8080/api/posts', values)
.then(() => callback());
return {
type: CREATE_POST,
payload: request
};
}
The callback should be responsible for the above redirection in this case. The client code which uses the action should be like this.
onSubmit(values) {
this.props.createPost(values, () => {
this.props.history.push('/');
});
}
This makes your action much more flexible and reusable too.
Then when you test it, you can pass a stub to the action, and verify whether it is called once. Writing a quality, testable code is an art though.
The problem with your code is that the startAddPost function is a mock function which does not return a Promise, but your actual this.props.startAddPost function does return a Promise.
That's why your code works but fails when you try to test it, leading to the cannot read property.... error.
To fix this make your mocked function return a Promise like so -
beforeEach(() => {
startAddPost = jest.fn().mockReturnValueOnce(Promise.resolve())
...
});
Read more about mockReturnValueOnce here.

Resources