mesamess
mesamess

Reputation: 83

Express/Typescript testing with Jest/Supertest

I'm currently trying to test an express API,I'm using Jest and Supertest however I can't seem to get it to work.

My code is :

router.get('/', async (req: Request, res: Response) => {
  const products: ProductType[] = await ProductModel.find({});

  res.send(products);
});

My test is :

describe('GET /', () => {
  it('calls ProductModel.find and returns products', async () => {
    const mockproducts = 'this is a product';

    ProductModel.find = jest.fn().mockResolvedValueOnce(mockproducts);

    const response = await request(products).get('/');

    expect(response).toBe(mockproducts);
  });
});

So basically, the mocked resolve value is all working fine but when I run the test, the res.send is not working.

TypeError: res.send is not a function

Could anyone advise what the problem is here?

Thanks!

Upvotes: 1

Views: 2531

Answers (1)

Isolated
Isolated

Reputation: 2453

Could anyone advise what the problem is here?

You're using supertest in unit testing when it can be avoided. supertest also accepts an instance of your express application, and it appears products is provided? Or is products your express instance? Another problem you may find is that ProductModel.find isn't mocked until after the test has called as you're using global instances.

When testing, we can make our lives much easier by designing our code with clear abstractions and testing in mind.

Dependencies

When you design your code, design code to accept dependency instances as arguments/properties:


// as an argument
function makeHttpRequest(path, httpClient: AxoisInstance) {
  return httpClient.get(path);
}

// as a property of object/class
class DependsOn {
  constructor(private readonly httpClient: AxoisInstance) {}

  request(path: string) {
    return this.httpClient.get(path);
  }
}

This makes our testing easier as we can confidently say the correct instance (real or mock) has been provided to the controller, service, repository, and so on.

This also avoids using things like:


// ... some bootstrap function
if (process.env.NODE_ENV === 'test') {
  someInstance = getMockInstance()
} else {
  someInstance = RealInstance();
}

Separate Concerns

When you're handling requests there's a few things that need to happen:

  1. Routing (mapping route handlers)
  2. Controllers (your route handler)
  3. Services (interacts with Repositories/Models/Entities)
  4. Model (your ProductModel, or data layer)

You currently have all of these inline (as I think 99.99% of us do when we pick up Express).


// product.routes.ts
router.get('/', ProductController.get); // pass initialised controller method

// product.controller.ts
class ProductController {
   constructor(private readonly service: ProductService) {}

   get(request: Request, response: Response) {
      // do anything with request, response (if needed)
      // if you need validation, try middleware
      response.send(await this.service.getAllProducts());
   }
}

// product.service.ts
class ProductService {
  // Model IProduct (gets stripped on SO)
  constructor(private readonly model: Model) {}
  
  getAllProducts() {
    return this.model.find({});
  }
}

Testing

We're now left several components we can easily test to ensure the correct input produces the correct output. In my opinion, jest is one of the easiest tools to mock methods, classes, and everything else providing you have good abstractions allowing you to do so.


// product.controller.test.ts
it('should call service.getAllProducts and return response', async () => {
  const products = [];
  const response = {
    send: jest.fn().mockResolvedValue(products),
  };

  const mockModel = {
    find: jest.fn().mockResolvedValue(products),
  };

  const service = new ProductService(mockModel);
  const controller = new ProductController(service);

  const undef = await controller.get({}, response);
  expect(undef).toBeUndefined();

  expect(response.send).toHaveBeenCalled();
  expect(response.send).toHaveBeenCalledWith(products);
  expect(mockModel.find).toHaveBeenCalled();
  expect(mockModel.find).toHaveBeenCalledWith();
});

// product.service.test.ts
it('should call model.find and return response', async () => {
  const products = [];

  const mockModel = {
    find: jest.fn().mockResolvedValue(products),
  };

  const service = new ProductService(mockModel);
  const response = await service.getAllProducts();

  expect(response).toStrictEqual(products);
  expect(mockModel.find).toHaveBeenCalled();
  expect(mockModel.find).toHaveBeenCalledWith();
});

// integration/e2e test (app.e2e-test.ts) - doesn't run with unit tests
// test everything together (mocking should be avoided here)
it('should return the correct response', () => {
  return request(app).get('/').expect(200).expect(({body}) => {
    expect(body).toStrictEqual('your list of products')
  });
})

For your application, you'll need to determine a suitable way of injecting dependencies into the correct classes. You may decide a main function that accepts the required models works for you, or may decide that something more powerful like https://www.npmjs.com/package/injection-js would work.

Avoiding OOP

If you'd ideally like to avoid using objects, accept instances as a function argument: productServiceGetAll(params: SomeParams, model?: ProductModel).

Learn More

  1. https://www.guru99.com/unit-testing-guide.html
  2. https://jestjs.io/docs/mock-functions
  3. https://levelup.gitconnected.com/typescript-object-oriented-concepts-in-a-nutshell-cb2fdeeffe6e?gi=81697f76e257
  4. https://www.npmjs.com/package/supertest
  5. https://tomanagle.medium.com/strongly-typed-models-with-mongoose-and-typescript-7bc2f7197722

Upvotes: 2

Related Questions