useSearchParams hook provides flexibility to read, check, modify and delete query parameters at ease. But unfortunately, I found it hard to unit test it. Hence this post 😄
let's start with a code snippet..
// sampleComponent.js
import { useSearchParams } from 'react-router-dom';
function SampleComponent() {
const [searchParams, setSearchParams] = useSearchParams();
const updateParam = () => {
const updatedSearchParams = new URLSearchParams(searchParams.toString());
if (updatedSearchParams.has('userName')) {
updatedSearchParams.delete('userName');
} else {
updatedSearchParams.set("userName", "testA");
}
setSearchParams(updatedSearchParams.toString());
}
return (
<div data-testid='container'>
userName in searchParams {searchParams.get("userName")}
<button data-testid='button' onClick={updateParam}>toggle username in params</button>
</div>
);
}
in above code we are printing userName
from serchParams
. On button click, we're toggling userName
in queryparams. Most of the times it is not this simple, as we depend on searchParams
to perform some complex operations. Now these things make very hard to test in our unit tests if the searchParams is getting updated properly or not.
Whining apart. let's write unit test for above component in jest.
// sampleComponent.spec.js
import {
render,
screen
} from "@testing-library/react";
const Wrapper = () => {
return <MemoryRouter>
<SampleComponent />
</MemoryRouter>
}
describe("SampleComponent", () => {
it('should render component successfully', () => {
render(<Wrapper />);
expect(screen.getByTestId("container")).toBeInTheDocument();
});
});
very basic test. nothing special. As we're using useSearchParams
hook, we wrapped our component inside MemoryRouter
and checked if container
is there in the document or not.
1st Step is done. Now in 2nd step, we need to mock useSearchParams to test the functionality.
let's modify the above spec as below:
// sampleComponent.spec.js
import {
render,
screen
} from "@testing-library/react";
let mockSearchParam = '';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useSearchParams: () => {
const [params, setParams] = useState(new URLSearchParams(mockSearchParam));
return [
params,
(newParams: string) => {
mockSearchParam = newParams;
setParams(new URLSearchParams(newParams));
}
];
}
}));
const Wrapper = () => {
return <MemoryRouter>
<SampleComponent />
</MemoryRouter>
}
describe("SampleComponent", () => {
it('should render component successfully', () => {
render(<Wrapper/>);
expect(screen.getByTestId("container")).toBeInTheDocument();
})
});
cool. We mocked the react-router-dom
package partially overriding the implementation of useSearchParams.
If you observe closely, we declared a global variable mockSearchParam
and used useState
inside our mock implementation. the mock returns an array that contains the state and a function to update the state. Whenever we call setSearchParams, it update the global variable value which helps us to check if it is getting updated as per expected.
As we're using useState
in mockImplementation, it re-renders our component whenever we clicked the button. This is the party trick 😎
Now let's write test for updateParam
function in our component.
// sampleComponent.spec.js
...
it("should toggle userName in searchParams on button click", () => {
render(</Wrapper />);
const button = screen.getBytestId('button');
fireEvent.click(button);
// check if it is adding userName to searchParams
expect(mockSearchParam).toContain('userName=testA');
fireEvent.click(button);
// check if it removed userName from searchParams
expect(mockSearchParam).not.toContain('userName=testA');
});
...
Excellant. let me tell you how it worked behind the screens. In our spec file, we mocked the implementation of useSearchParams. when our component renders, it read the value from our mock implementation. Now when we trigger click event on button, it fires setParams
function in our mock implementation of useSearchParams. along with that, it also update the global mockSearchParam
value too. This helps us to read the updated query param string via the global variable in our test.
This is how the final spec file looks like:
// sampleComponent.spec.js
import {
render,
screen
} from "@testing-library/react";
import { useState } from 'react';
let mockSearchParam = '';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useSearchParams: () => {
const [params, setParams] = useState(new URLSearchParams(mockSearchParam));
return [
params,
(newParams) => {
mockSearchParam = newParams;
setParams(new URLSearchParams(newParams));
}
];
}
}));
const Wrapper = () => {
return <MemoryRouter>
<SampleComponent />
</MemoryRouter>
}
describe("SampleComponent", () => {
it('should render component successfully', () => {
render(<Wrapper />);
expect(screen.getByTestId("container")).toBeInTheDocument();
});
it("should toggle userName in searchParams on button click", () => {
render(</Wrapper />);
const button = screen.getBytestId('button');
fireEvent.click(button);
// check if it is adding userName to searchParams
expect(mockSearchParam).toContain('userName=testA');
fireEvent.click(button);
// check if it removed userName from searchParams
expect(mockSearchParam).not.toContain('userName=testA');
});
});
There's definetly a different way to do this too. Don't forget to share that in comments below.
And that's the wrap.
Hope this helps you in your TDD.
See you again,
Kiran 👋
Top comments (5)
Getting the following error:
Can create an example repo?? I will check it out
I was getting the same error, but I was able to work around it by bringing mockSearchParams into the mocked
useSearchParams
function and manually updating the variableparams
like this:Hi, where do you import useState inside mock implementation?
Hi @morelir , probably I missed that import in spec file. Thanks for pointing out. I'll update the article shortly.