How to unit test useSearchParams?
Tags: react, unittest, programming
Published: 2023-05-11 04:36:32

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 :smile:

let's start with a code snippet..

```javascript
// 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.

```javascript
// 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:

```javascript
// 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** :sunglasses:

Now let's write test for `updateParam` function in our component.

```javascript
// 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:

```javascript
// 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 :wave: