DEV Community

Kiran Mantha
Kiran Mantha

Posted on • Edited on

How to unit test useSearchParams?

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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();
    });
});
Enter fullscreen mode Exit fullscreen mode

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();
    })
});
Enter fullscreen mode Exit fullscreen mode

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');
});

...
Enter fullscreen mode Exit fullscreen mode

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');
    });
});

Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
savandy profile image
savANDY

Getting the following error:

The module factory of jest.mock() is not allowed to reference any out-of-scope variables.
Invalid variable access: useState

Collapse
 
kiranmantha profile image
Kiran Mantha

Can create an example repo?? I will check it out

Collapse
 
catalina_mcquade_25ae45a4 profile image
Catalina McQuade

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 variable params like this:

  useSearchParams: () => {
    let mockSearchParams = ''
    return [
      new URLSearchParams(mockSearchParams),
      (newParams: string) => {
        mockSearchParams = newParams
      }
    ]
  }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
morelir profile image
Mor Elir

Hi, where do you import useState inside mock implementation?

Collapse
 
kiranmantha profile image
Kiran Mantha

Hi @morelir , probably I missed that import in spec file. Thanks for pointing out. I'll update the article shortly.