Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 35

Testing POST Endpoint: Success and Validation

Recommended resources

Introduction

In our last lesson, you built a comprehensive test suite for your GET endpoints, learning how to verify success (200), "not found" (404), and "bad request" (400) scenarios by controlling Mongoose's behavior with mocks.

This lesson continues that vital work by focusing on the POST endpoint. This is where we test the creation of new resources. Unlike GET requests which only read data, POST requests change the state of our application. Therefore, our testing must cover two critical paths:

  1. Successful Creation: Does the API correctly create a new resource when given valid data and return a 201 Created status?
  2. Validation Failure: Does the API correctly reject invalid data with a 400 Bad Request status before attempting to save to the database?

By the end of this lesson, you will have the patterns to confidently test any resource creation endpoint.


1. Testing Successful Creation (The "Happy Path")

The goal here is to simulate a client sending valid data to create a new product. Your test needs to confirm that the controller correctly calls the Mongoose create method and sends back the newly created document with the appropriate 201 status code.

The Test Flow:

  1. Setup: Define the payload (the new product data) and the expected result from the database. Mock the Product.create() method to resolve with this expected result.
  2. Action: Use Supertest to send a POST request to /api/products with the payload.
  3. Assertion: Verify that the status code is 201, the response body matches the created product, and that the Product.create mock was called with the correct data.

Let's implement this.

// In tests/routes/productRoutes.test.js

describe('POST /api/products', () => {
  it('should create a new product and return 201 status', async () => {
    // 1. Setup
    const newProductData = {
      name: 'Wireless Headphones',
      price: 150,
      description: 'Noise-cancelling over-ear headphones'
    };
    const savedProduct = {
      _id: '60d21b4667d0d8992e610c87', // Mocked ID from the database
      ...newProductData
    };
    Product.create.mockResolvedValue(savedProduct);

    // 2. Action
    const res = await request(server)
      .post('/api/products')
      .send(newProductData); // .send() is used to provide the request body

    // 3. Assertion
    expect(res.statusCode).toBe(201);
    expect(res.body).toEqual(savedProduct);
    expect(Product.create).toHaveBeenCalledWith(newProductData);
  });
});

This test gives us high confidence. We know the right status is returned, the created object is sent back to the client, and our controller is correctly interacting with the model.

Integration Test for POST Endpoint (Successful Creation)
This code snippet illustrates the standard pattern for testing a successful POST request. Notice the use of `.post()`, `.send()`, and the assertions for both the status code (201) and the response body.

To see this pattern in action, watch the first part of this video. It demonstrates testing a successful POST request and asserting the 201 status.

Supertest express tutorial | Node REST API testing

Watch this segment from the "Supertest express tutorial" by Jan Goebel to see a live demonstration of testing a successful POST request.

Watch from 04:53 to 06:55. The video shows how to use .post(), .send() a payload, and then expect() the status code to be 201. This is the exact pattern we are implementing.


2. Testing Validation Failure (The "Sad Path")

A robust API must reject invalid data. In our case, a name is required to create a product. Your validation logic (likely in the controller or a middleware) should catch this and return a 400 Bad Request without ever trying to call the database.

The Test Flow:

  1. Setup: Define an invalid payload (e.g., missing the name field).
  2. Action: Use Supertest to send a POST request with the invalid payload.
  3. Assertion: Verify the status code is 400. Crucially, also verify that Product.create() was not called. This confirms your validation is working correctly and preventing unnecessary database calls.

Here's the implementation:

// Add this 'it' block inside your 'POST /api/products' describe block

it('should return 400 status if required fields are missing', async () => {
  // 1. Setup: Invalid data (missing name)
  const invalidData = { price: 99 };

  // 2. Action
  const res = await request(server)
    .post('/api/products')
    .send(invalidData);

  // 3. Assertion
  expect(res.statusCode).toBe(400);
  
  // Optional but highly recommended: Assert the error message
  expect(res.body.message).toBe('Name is required'); // Adjust to your actual error message

  // Most important: Ensure the database was not touched
  expect(Product.create).not.toHaveBeenCalled();
});

This second video clip demonstrates this exact scenario, showing how to test for a 400 status and check the error message in the response body.

Supertest express tutorial | Node REST API testing

Now, watch the second part of the same video, which covers testing the error case.

Watch from 07:21 to 08:31. This shows how to send an incomplete payload and assert that the server correctly responds with a 400 status and the expected error message.

For a more comprehensive approach, especially when you have multiple required fields, you can test various invalid payloads in a single test case. The article "Automated Testing with Jest and Supertest in Node.js" provides a clean, text-based example of both the success and failure tests side-by-side.

Automated Testing with Jest and Supertest in Node.js

This article provides excellent, concise code examples for testing a POST endpoint. Review the provided test suite for creating a user.

In the section "Complete test suites", look at the describe("POST /api/v1/users", ...) block. It contains two tests: one that asserts a 400 status when data is missing, and another that asserts a 201 status when data is provided. This is a perfect template for your own tests.


Conclusion

You have now mastered the essential patterns for testing a POST endpoint. You can verify both successful resource creation and a graceful rejection of invalid data. Your test suite is becoming a powerful safety net, ensuring your API behaves as expected.

Key Takeaways:

  • Test for Success (201):
    • Mock Model.create() to return a document.
    • Use .post('/path').send(validData).
    • Assert status is 201.
    • Assert the response body matches the created document.
  • Test for Validation Failure (400):
    • Send an incomplete or invalid payload using .send(invalidData).
    • Assert status is 400.
    • Assert Model.create() was not called.

Next Up:

With GET (read) and POST (create) operations fully tested, the next logical steps are to test the endpoints that modify existing data. In the following lesson, we will write integration tests for your PUT (update) and DELETE (remove) endpoints, covering their unique success and failure scenarios.