1

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!

1 Answer 1

2

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
Sign up to request clarification or add additional context in comments.

4 Comments

Wow, this is an amazing answer! This architecture really reminds me of NestJs. I can see now that I need to put more thought into my code architecture as Express can get a bit wild. I didn't even know that Express can be tested without Supertest.
@mesamess There's a reason Nest.js provides the architecture it does! I spent a while with Express before switching and look at it as an opinionated wrapper around it to make more robust/testable software. If you're new, Express may be the better option though as the wildness is flexibility to create an architecture that makes sense to you, and see how it works during testing/deployments. All the best!
Could I ask one more question if that's alright. I'm trying to implement the ProductService class, however in the constructor, this.model = model says model does not exist on type ProductService, and the parameter for the constructor (model: ProductModel) says it refers to a value and not a type. So I want to check with you whether Product Model should be an interface or something, because at the moment it is a schema for Mongoose.
@mesamess that would be my fault, I had originally answered it in JS and hadn't updated it in TS (updated now). To solve your last problem: tomanagle.medium.com/… should help! But you're correct, it needs an interface. (I've added this link to my answer as well).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.