supertest icon indicating copy to clipboard operation
supertest copied to clipboard

How can I test dynamic next.js API route using supertest in an integration test scenario?

Open riha opened this issue 2 years ago • 9 comments

I have this simple example of testing an API route using supertest. I want to do an "integration test". So I want to hit the actual API, execute all the code in the API handler and test the result.

It works fine until I try to read a query parameter in a dynamic API route.

Repo: https://github.com/riha/test-supertest

Next.js dynamic API /api/[name].ts

import type { NextApiRequest, NextApiResponse } from "next";

type Data = {
  name: string;
};

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  console.log(req);
  // nothing get inserted into req.query 🥺
  const name = req.query.name as string;
  res.status(200).json({ name: name });
}

And the test __tests__/api/name.ts

const testClient = (handler: NextApiHandler) => {
  const listener: RequestListener = (req, res) => {
    return apiResolver(
      req,
      res,
      undefined,
      handler,
      {
        previewModeEncryptionKey: "",
        previewModeId: "",
        previewModeSigningKey: "",
      },
      false
    );
  };

  return request(createServer(listener));
};

it("What's my name!?", async () => {
  const client = testClient(handler);
  const response = await client.get("/api/name").query({ name: "John" });

  console.log(response.body);

  expect(true);
});

Test API gets called but the query parameter isn't parsed into the request object. So req.query is always undefined. I'm sure this is obvious to some? I guess I can't call the handler this way and that I somehow miss some next.js magic middleware?

But how can I do it differently? Could I start a server and a port listener and call it using that somehow? Could I somehow create the handler differently? Is there a way where I could also add the data to the request param object, before sending it, I guess that could work as well?

riha avatar Apr 01 '22 09:04 riha

You can pass query parameters to your api end-point by specifying them as third parameter of the apiResolver:

const testClient = (handler: NextApiHandler) => {
  const listener: RequestListener = (req, res) => {
    return apiResolver(
      req,
      res,
      { name: "John" }, // Query params
      handler,
      {
        previewModeEncryptionKey: "",
        previewModeId: "",
        previewModeSigningKey: "",
      },
      false
    );
  };

  return request(createServer(listener));
};

You can then change your test code to:

it("What's my name!?", async () => {
  const client = testClient(handler);
  const response = await client.get("/api/name");

  console.log(response.body);

  expect(true);
});

You can find the declaration of the apiResolver here.

lpizzinidev avatar May 30 '22 13:05 lpizzinidev

Hey, was anybody able to come to the bottom of this 🤔 Since we are passing handler "directly" it doesn't matter which endpoint url gets passed const response = await client.get("/api/name-slug"); where query params should be passed to actual request getting called client.get("/api/name").query({param1: 'testValue'}) (not passing/hardcoding it to testClient)

mkosir avatar Jul 26 '22 11:07 mkosir

Still have yet to completely solve this issue, but passing the query to testClient seems like a valid workaround.

Update your testClient with the following:

export const queryClient = (handler: NextApiHandler, query: object) => {
  const listener: RequestListener = (req, res) => {
    return apiResolver(
      req,
      res,
      query,
      handler,
      {
        previewModeEncryptionKey: '',
        previewModeId: '',
        previewModeSigningKey: '',
      },
      false
    );
  };

  return request(createServer(listener));
};

and you can fulfill the request with such:

  it('should GET and return a 200 status code', async () => {
    const res = await queryClient(getUserIdHandler, { id: 'hi' }).get('/');
    expect(res.status).toEqual(200);
  });

Happy coding :>

stevenluongo avatar Nov 13 '22 18:11 stevenluongo

Still have yet to completely solve this issue, but passing the query to testClient seems like a valid workaround.

Update your testClient with the following:

export const queryClient = (handler: NextApiHandler, query: object) => {
  const listener: RequestListener = (req, res) => {
    return apiResolver(
      req,
      res,
      query,
      handler,
      {
        previewModeEncryptionKey: '',
        previewModeId: '',
        previewModeSigningKey: '',
      },
      false
    );
  };

  return request(createServer(listener));
};

and you can fulfill the request with such:

  it('should GET and return a 200 status code', async () => {
    const res = await queryClient(getUserIdHandler, { id: 'hi' }).get('/');
    expect(res.status).toEqual(200);
  });

Happy coding :>

The problem with this method is that you need to create an http instance for each handler, and it is not matching with the URL you're giving to supertest. It will work either with .get('/toto') or with .get('/abcd').

But don't worry, I've just released an npm package which handle this case: https://www.npmjs.com/package/nextjs-http-supertest

Feel free to use it and give me feedback !

mtbrault avatar Jan 16 '23 15:01 mtbrault

Hey, was anybody able to come to the bottom of this 🤔 Since we are passing handler "directly" it doesn't matter which endpoint url gets passed const response = await client.get("/api/name-slug"); where query params should be passed to actual request getting called client.get("/api/name").query({param1: 'testValue'}) (not passing/hardcoding it to testClient)

Yep I did, just take a look at my reply just above 👍 https://www.npmjs.com/package/nextjs-http-supertest

mtbrault avatar Jan 16 '23 15:01 mtbrault

Hey, was anybody able to come to the bottom of this thinking Since we are passing handler "directly" it doesn't matter which endpoint url gets passed const response = await client.get("/api/name-slug"); where query params should be passed to actual request getting called client.get("/api/name").query({param1: 'testValue'}) (not passing/hardcoding it to testClient)

Yep I did, just take a look at my reply just above +1 https://www.npmjs.com/package/nextjs-http-supertest

Doesn't work for me :( image

And If I try create the server manually as the first response, I have problems with the env variables, but if I test with Postman everything work

LksR-dev avatar Feb 01 '23 00:02 LksR-dev

Hey, was anybody able to come to the bottom of this thinking Since we are passing handler "directly" it doesn't matter which endpoint url gets passed const response = await client.get("/api/name-slug"); where query params should be passed to actual request getting called client.get("/api/name").query({param1: 'testValue'}) (not passing/hardcoding it to testClient)

Yep I did, just take a look at my reply just above +1 https://www.npmjs.com/package/nextjs-http-supertest

Doesn't work for me :( image

And If I try create the server manually as the first response, I have problems with the env variables, but if I test with Postman everything work

Can you please provide more code about how did you init your server ? (The best should be a link to your github repo)

mtbrault avatar Feb 01 '23 09:02 mtbrault

Hey, was anybody able to come to the bottom of this thinking Since we are passing handler "directly" it doesn't matter which endpoint url gets passed const response = await client.get("/api/name-slug"); where query params should be passed to actual request getting called client.get("/api/name").query({param1: 'testValue'}) (not passing/hardcoding it to testClient)

Yep I did, just take a look at my reply just above +1 https://www.npmjs.com/package/nextjs-http-supertest

Doesn't work for me :( image And If I try create the server manually as the first response, I have problems with the env variables, but if I test with Postman everything work

Can you please provide more code about how did you init your server ? (The best should be a link to your github repo)

Yeah, of course. Not up to date but basically I import de package on the test file. Repo: https://github.com/LksR-dev/crud-nextjs-ts

LksR-dev avatar Feb 01 '23 12:02 LksR-dev

Hi,

I am trying to test my NextJS APIs using your approach. It seems to be return,

`● Test suite failed to run

ReferenceError: Headers is not defined

  at Object.<anonymous> (node_modules/next/dist/server/web/spec-extension/adapters/headers.js:32:30)
  at Object.<anonymous> (node_modules/next/dist/server/api-utils/index.js:67:18)`

Does anyone know how to fix this or why this error is showing up in the first place. My Next.js version is 13.3.4.

Another question I have is, when not using your approach, it almost works expect in one of my APIs I am redirecting to another localhost API which handles some specific logic. example: From http://localhost:3000/api/posts (which is the handler I am targeting in my tests) I am calling http://localhost:3000/api/formatPosts and this seems to return a ECONNREFUSED error. I thought supertest was supposed to be running a temporary server for my API tests but it doesn't seem to be. Please help with this approach as well

lalitIrdeto avatar Sep 26 '23 16:09 lalitIrdeto