Jest Unit Testing - Part 2

Route Handlers

I created a simple route handler to retrieve latitude and longitude from an address, using Google's Geolocation API. Remember, route handlers in Next.js allow you to create custom request handlers for a given route, and they are always defined in a file named route.ts or route.js inside the app directory. Route handlers use the Web Request and Response APIs and it was these objects that gave me troubles while trying to write unit tests for the handler. Here is the route handler:

app/api/geolocation/route.ts

export const dynamic = 'force-dynamic' // defaults to auto

export async function POST(req: Request) {
    const {address} = await req.json();
    const apiKey = process.env.GOOGLE_MAPS_API_KEY;
    const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${apiKey}`;

    try {
        const response = await fetch(url, {
            method: 'GET',
            headers: { 'Content-Type': 'application/json' },
        });
    const data = await response.json();
    const coordinates = data.results[0]?.geometry?.location;

    return Response.json({ coordinates })
  } catch (error) {
      return Response.json({ error: 'Failed to get coordinates from address'});
  }
}

To test this handler, you need to mock both the Request and Response. For Request, I used httpMocks. To use it, install node-mocks-http and then import into your test file: httpMocks, { MockRequest } from 'node-mocks-http';

const req = httpMocks.createRequest<MockRequest<any>>({
        key: 'value',
        method: 'POST',
        url: '/api/geolocation',
        body: {
          address: '1 Main St, Miami, FL',
        },
        json() {
          return this.body;
        }
      });

Once the Request is mocked correctly you may encounter this error: ReferenceError: Response is not defined. After some research and a lot of trial and error, I learned that installing and importing isomorphic-fetch fixes this error. If you try running the test with the import, the error changes to: TypeError: Response.json is not a function. At this point, the Response object is defined but not its json() function, so you just need to mock it:

Response.json = jest.fn().mockResolvedValue({
          coordinates: { lat: '123', lng: '456' },
      });

Putting it all together, here is the full happy path test:

app/api/geolocation/route.test.ts

import { POST } from './route';
import httpMocks, { MockRequest } from 'node-mocks-http';
// you can also import this into jest.setup.ts so it can be 
// available to all tests
import 'isomorphic-fetch';

describe('Geolocation handler', () => {
    it('should return coordinates when given a valid address', async () => {
      global.fetch = jest.fn().mockResolvedValue({
        json: jest.fn().mockResolvedValue({
          results: [
            {
              geometry: {
                location: {
                  lat: '123',
                  lng: '456',
                },
              },
            },
          ],
        }),
      });

      Response.json = jest.fn().mockResolvedValue({
          coordinates: { lat: '123', lng: '456' },
      });

      const req = httpMocks.createRequest<MockRequest<any>>({
        key: 'value',
        method: 'POST',
        url: '/api/geolocation',
        body: {
          address: '1 Main St, Miami, FL',
        },
        json() {
          return this.body;
        }
      });      
      const res = await POST(req);

      expect(res).toEqual({ coordinates: { lat: '123', lng: '456' } });
    });

I hope you found this helpful. Have you found a better of testing route handlers? Please share in the comments!

Happy coding!