How to mock URLSearchParams and searchParams with Jest

Peter Jacxsens - Jul 12 '23 - - Dev Community

In the previous part we setup an example of a little project that uses the searchParams page prop, the useSearchParams hook and the useRouter hook. We went over all the files to get a good understanding how everything works.

In this part, we will test all of the files, functions and hooks. The files and test can be found on github.

Overview

Components to test:

  • page.js
  • <List />
  • <ListControlesButtons />

Functions to test:

  • getSortOrderFromSearchParams()
  • getSortOrderFromUseSearchParams()
  • validateSortOrder()

Custom Hook:

  • useSort()

As we talked about in the previous chapter, I wrote the code in such a way that searchParams, useSearchParams and useRouter are nicely separated from our jsx in functions and a custom hook. Testing these functions will be the focus of this part. In the third part we will test our custom hook.

While we will not review the tests of our components, they do exist. You can view them in on github. The test files are in a __test__ folder on the same root as the component files.

validateSortOrder()

This function receives a value, checks if this value equals asc or desc and returns that boolean. It's also a type predicate. When returning true, Typescript will threat values as of type SortOrder. But that doesn't matter for testing. Here's the function:

// lib/validateSortOrder.ts

import { SortOrder } from '@/types';

export default function validateSortOrder(value: string): value is SortOrder {
  const validOptions: [SortOrder[0], SortOrder[1]] = ['asc', 'desc'];
  return validOptions.includes(value as SortOrder);
}
Enter fullscreen mode Exit fullscreen mode

The test is pretty straightforward. We test 4 scenario's: when value is asc or desc, the function returns true. When value is invalid: foobar of undefined, the function returns false. No tricks, no magic, just a simple test.

// lib/__test__/validateSortOrder.test.js

import validateSortOrder from '../validateSortOrder';

describe('@/lib/isValidSortOrder', () => {
  test('It correctly validates "asc"', () => {
    const result = validateSortOrder('asc');
    expect(result).toBe(true);
  });

  test('It correctly validates "desc"', () => {
    const result = validateSortOrder('desc');
    expect(result).toBe(true);
  });

  test('It returns invalid for value undefined', () => {
    const result = validateSortOrder(undefined);
    expect(result).toBe(false);
  });

  test('It returns invalid for value "foobar"', () => {
    const result = validateSortOrder('foobar');
    expect(result).toBe(false);
  });
});
Enter fullscreen mode Exit fullscreen mode

getSortOrderFrom...

getSortOrderFromSearchParams() and getSortOrderFromUseSearchParams() are twin functions. They do the same thing but they take a different type of parameter. The first takes an object (from searchParams page prop), the second takes an URLSearchParams object (from useSearchParams hook).

The goal of these functions is to take a url query and:

  • Check if this query has a prop called sortOrder.
  • Check if the value of this sortOrder props is not empty.

If these conditions are met, these functions call validateSortOrder, else they return a default value: asc.

// lib/getSortOrderFromSearchParams.ts

import { SearchParams, SortOrder } from '@/types';
import validateSortOrder from './validateSortOrder';

export default function getSortOrderFromSearchParams(
  searchParams: SearchParams
): SortOrder {
  const sortOrderParam = searchParams.sortOrder;
  let sortOrder: SortOrder = 'asc';
  if ('sortOrder' in searchParams && sortOrderParam) {
    if (validateSortOrder(sortOrderParam)) {
      sortOrder = sortOrderParam;
    }
  }
  return sortOrder;
}
Enter fullscreen mode Exit fullscreen mode
// lib/getSortOrderFromUseSearchParams.ts

import { SortOrder } from '@/types';
import { ReadonlyURLSearchParams } from 'next/navigation';
import validateSortOrder from '../lib/validateSortOrder';

export default function getSortOrderFromUseSearchParams(
  params: ReadonlyURLSearchParams
) {
  const sortOrderParam = params.get('sortOrder');
  let sortOrder: SortOrder = 'asc';
  if (params.has('sortOrder') && sortOrderParam) {
    if (validateSortOrder(sortOrderParam)) {
      sortOrder = sortOrderParam;
    }
  }
  return sortOrder;
}
Enter fullscreen mode Exit fullscreen mode

getSortOrderFromSearchParams()

Let's test the first one first. This is the one that takes an object, f.e. { sortOrder: 'asc' }. We already tested validateSortOrder so we will mock this function and return a value from this mock (true of false). We run every possible combination. Notice how we 'mock' our object by writing an object ourselves:

  • No sortOrder param: {}
  • sortOrder with empty or undefined value: { sortOrder: '' }
  • sortOrder with non-empty values { sortOrder: 'foobar' } + true or false return values from the validateSortOrder mock.
// lib/__test__/getSortOrderFromSearchParams.test.js

import getSortOrderFromSearchParams from '../getSortOrderFromSearchParams';
import validateSortOrder from '../validateSortOrder';

jest.mock('../validateSortOrder');

describe('@/lib/getSortOrderFromSearchParams', () => {
  test('It returns default "asc" when no sortOrder property', () => {
    validateSortOrder.mockReturnValue(true);
    const result = getSortOrderFromSearchParams({});
    expect(result).toBe('asc');
  });

  test('It returns default "asc" when sortOrder is empty', () => {
    validateSortOrder.mockReturnValue(true);
    const result = getSortOrderFromSearchParams({ sortOrder: '' });
    expect(result).toBe('asc');
  });

  test('It returns default "asc" when sortOrder is undefined', () => {
    validateSortOrder.mockReturnValue(true);
    const result = getSortOrderFromSearchParams({ sortOrder: undefined });
    expect(result).toBe('asc');
  });

  test('It returns default "asc" when validateSortOrder returns false', () => {
    validateSortOrder.mockReturnValue(false);
    const result = getSortOrderFromSearchParams({ sortOrder: 'foobar' });
    expect(result).toBe('asc');
  });

  test('It returns sortOrder value when validateSortOrder returns true', () => {
    validateSortOrder.mockReturnValue(true);
    const result = getSortOrderFromSearchParams({ sortOrder: 'foobar' });
    expect(result).toBe('foobar');
  });
});
Enter fullscreen mode Exit fullscreen mode

getSortOrderFromUseSearchParams()

As said earlier, we don't pass an object but an URLSearchParams object to the second function getSortOrderFromUseSearchParams. So, we will have to 'mock' this interface somehow. How do we do this? We just write a object that has all the methods that are used in our function.

Our function getSortOrderFromUseSearchParams uses the methods .has() and .get(). So, the test object we pass into our function needs to have these 2 methods:

{
  get: () => 'some value',
  has: () => true // (or false)
}
Enter fullscreen mode Exit fullscreen mode

As we need to run multiple tests on this function, I wrote a setup function to keep things DRY:

function setup(value, hasValue) {
  const param = {
    get: () => value,
    has: () => hasValue,
  };
  return getSortOrderFromUseSearchParams(param);
}
Enter fullscreen mode Exit fullscreen mode

We call this function inside a test. We pass it the value we want to see returned from get and a boolean we want to return from has. Our setup function then returns the return value from getSortOrderFromUseSearchParams: asc | desc.

Here is an example of one of our test to make things clear:

test('It returns default "asc" when validateSortOrder returns false', () => {
  validateSortOrder.mockReturnValue(false);
  const result = setup('foobar', true);
  expect(result).toBe('asc');
});
Enter fullscreen mode Exit fullscreen mode

And here is our function again:

function getSortOrderFromUseSearchParams(params: ReadonlyURLSearchParams) {
  const sortOrderParam = params.get('sortOrder');
  let sortOrder: SortOrder = 'asc';
  if (params.has('sortOrder') && sortOrderParam) {
    if (validateSortOrder(sortOrderParam)) {
      sortOrder = sortOrderParam;
    }
  }
  return sortOrder;
}
Enter fullscreen mode Exit fullscreen mode

We call the setup function:

const result = setup('foobar', true);
Enter fullscreen mode Exit fullscreen mode

This equals:

const result = getSortOrderFromUseSearchParams({
  get: () => 'foobar',
  has: () => true,
});
Enter fullscreen mode Exit fullscreen mode

getSortOrderFromUseSearchParams receives our object and runs these has() and get() methods on this object.

As both the conditions are now met:

if(params.has('sortOrder') && sortOrderParam)
Enter fullscreen mode Exit fullscreen mode

getSortOrderFromUseSearchParams continues and calls validateSortOrder. But, we mocked that function and gave a return value of false.

validateSortOrder.mockReturnValue(false);
Enter fullscreen mode Exit fullscreen mode

This leads the following condition inside getSortOrderFromUseSearchParams to fail:

if (validateSortOrder(sortOrderParam)) {
  sortOrder = sortOrderParam;
}
Enter fullscreen mode Exit fullscreen mode

As this condition fails, sortOrder will not be overwritten (with 'foobar') and getSortOrderFromUseSearchParams will return the default value of asc. And that is what our test asserted:

expect(result).toBe('asc');
Enter fullscreen mode Exit fullscreen mode

Let's go over this one more time. getSortOrderFromUseSearchParams expects an object with a has() and a get() method. As we test this function in isolation we need to fabricate this object ourselves.

We setup a setup function where we return different values from the has() and get() methods. This allows us to test getSortOrderFromUseSearchParams in different scenario's.

Inside getSortOrderFromUseSearchParams we mocked validateSortOrder and set different return values on it along our needs. This makes the test a bit complex but flexible.

We end up testing the same cases we tested in the earlier twin function, using a different parameter. Here are all the tests for getSortOrderFromUseSearchParams:

// lib/__test__/getSortOrderFromUseSearchParams.test.js

import getSortOrderFromUseSearchParams from '../getSortOrderFromUseSearchParams';
import validateSortOrder from '../validateSortOrder';

jest.mock('../validateSortOrder');

function setup(value, hasValue) {
  const param = {
    get: () => value,
    has: () => hasValue,
  };
  return getSortOrderFromUseSearchParams(param);
}

describe('@/lib/getSortOrderFromUseSearchParams', () => {
  test('It returns default "asc" when no sortOrder property', () => {
    validateSortOrder.mockReturnValue(true);
    const result = setup('foobar', false);
    expect(result).toBe('asc');
  });

  test('It returns default "asc" when no sortOrder is empty', () => {
    validateSortOrder.mockReturnValue(true);
    const result = setup('', true);
    expect(result).toBe('asc');
  });

  test('It returns default "asc" when no sortOrder is undefined', () => {
    validateSortOrder.mockReturnValue(true);
    const result = setup(undefined, true);
    expect(result).toBe('asc');
  });

  test('It returns default "asc" when validateSortOrder returns false', () => {
    validateSortOrder.mockReturnValue(false);
    const result = setup('foobar', true);
    expect(result).toBe('asc');
  });

  test('It returns sortOrder value when validateSortOrder returns true', () => {
    validateSortOrder.mockReturnValue(true);
    const result = setup('foobar', true);
    expect(result).toBe('foobar');
  });
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

The main takeaway there is how we 'mocked' URLSeachParams. Our function used 2 methods from this object: get() and has(). By simply creating an object with these 2 methods, we successfully mocked URLSeachParams. On top of that we added flexibility with a setup function. This allowed us to test different scenarios.

In the third and last part we will test our custom useSort hook.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player