Skip to main content

Welcome to a Grasp lesson

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

Lesson 36

Testing PUT Endpoints: Successes and Not-Found Errors

Recommended resources

Introduction

In the previous lesson, we built tests for your POST endpoint, ensuring it correctly handles both successful resource creation (201 Created) and validation failures (400 Bad Request). You now have a solid foundation for testing endpoints that create data.

This lesson focuses on endpoints that modify existing data: PUT requests. You will learn how to write integration tests for your update endpoint, covering the two most critical scenarios:

  1. Successful Update: The API finds the resource and updates it, returning a 200 OK status.
  2. Not-Found Error: The API correctly returns a 404 Not Found status when a client tries to update a resource that doesn't exist.

Mastering these patterns is essential for ensuring the reliability of your API's update functionality under exam conditions.


1. Testing a Successful Update (200 OK)

The "happy path" for a PUT request involves a client providing a valid resource ID and a payload of new data. Your test must confirm that the Mongoose findByIdAndUpdate method is called correctly and that the API returns the updated document with a 200 OK status.

The Test Flow:

  1. Setup: Define the ID of the resource to be updated and the update payload. Mock Model.findByIdAndUpdate() to resolve with the final, updated document.
  2. Action: Use Supertest to send a PUT request to your endpoint (e.g., /api/products/:id) with the update payload.
  3. Assertion: Verify that the status code is 200, the response body matches the updated document, and that Model.findByIdAndUpdate was called with the correct ID and data.

Here's the pattern in code:

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

describe('PUT /api/products/:id', () => {
  it('should update the product and return 200 status', async () => {
    // 1. Setup
    const productId = '60d21b4667d0d8992e610c87';
    const updateData = {
      name: 'Premium Wireless Headphones',
      price: 199
    };
    const updatedProduct = {
      _id: productId,
      ...updateData,
      description: 'Original description' // Assuming description was not updated
    };

    // Mock the Mongoose method to return the updated document
    Product.findByIdAndUpdate.mockResolvedValue(updatedProduct);

    // 2. Action
    const res = await request(server)
      .put(`/api/products/${productId}`)
      .send(updateData);

    // 3. Assertion
    expect(res.statusCode).toBe(200);
    expect(res.body).toEqual(updatedProduct);

    // Verify the mock was called correctly
    expect(Product.findByIdAndUpdate).toHaveBeenCalledWith(
      productId,
      updateData,
      { new: true } // Assuming your controller uses this option
    );
  });
});

This test confirms your entire update logic, from the route handler to the model interaction, is working as expected for successful cases.


2. Testing Not-Found Errors (404 Not Found)

A robust API must gracefully handle requests to update resources that don't exist. Your controller should check if findByIdAndUpdate returns null and, if so, respond with a 404.

The Test Flow:

  1. Setup: Define an ID that does not correspond to any document in the database. Mock Model.findByIdAndUpdate() to resolve with null, simulating that no document was found.
  2. Action: Use Supertest to send a PUT request to /api/products/:id using the non-existent ID.
  3. Assertion: Verify that the status code is 404. It is also good practice to assert that your model's update method was called, but the controller handled the null response correctly.

Here's how to implement this critical "sad path" test:

// Add this 'it' block inside your 'PUT /api/products/:id' describe block

it('should return 404 status if product is not found', async () => {
  // 1. Setup
  const nonExistentId = '123456789012345678901234';
  const updateData = { name: 'This will fail' };

  // Mock the Mongoose method to return null
  Product.findByIdAndUpdate.mockResolvedValue(null);

  // 2. Action
  const res = await request(server)
    .put(`/api/products/${nonExistentId}`)
    .send(updateData);

  // 3. Assertion
  expect(res.statusCode).toBe(404);

  // Optional: check for a specific error message if your API provides one
  expect(res.body.message).toBe('Product not found'); // Adjust to your API's response
});

A successful test run for these scenarios provides visual confirmation that your code is robust. The goal is to see green checkmarks next to both your "successful update" and "return 404 if not found" tests.

Jest Test Results for Todo API Endpoints
This image shows a successful test run from a real project. Notice the tests for the `PUT` endpoint, which include a successful update and a `404` case, both passing. This is the outcome you are aiming for.

A Complete Implementation Pattern

For a complete, exam-ready implementation pattern, the following resource provides an excellent and concise test suite for a PUT endpoint. It covers not only the 200 and 404 cases we've discussed, but also 400 errors for invalid ObjectIDs and missing request bodies, which you should also be prepared to handle.

Automated Testing with Jest and Supertest in Node.js

Study this example closely. It contains the exact test patterns you need for your exam. Focus on the describe("PUT /api/v1/users/:id", ...) block.

Navigate to the section titled "Complete test suites" and find the describe block for PUT /api/v1/users/:id. Analyze the four test cases: invalid ObjectId (400), user not found (404), missing data (400), and successful update (200). This is a comprehensive template for your own PUT endpoint tests.

By replicating these patterns, you build a powerful safety net that validates your API's update logic against all primary success and failure conditions.


Conclusion

You have now learned the essential patterns to write effective integration tests for PUT endpoints. Your test suite can now verify that your API updates resources correctly and handles attempts to update non-existent resources gracefully.

Key Takeaways:

  • Test for Success (200):
    • Mock Model.findByIdAndUpdate() to resolve with an updated document.
    • Use .put('/path/:id').send(updateData).
    • Assert the status is 200 and the response body matches the updated document.
  • Test for Not Found (404):
    • Mock Model.findByIdAndUpdate() to resolve with null.
    • Use .put('/path/:nonExistentId').
    • Assert the status is 404.

Next Up:

The final piece of our CRUD testing puzzle is the DELETE endpoint. In the next lesson, you will learn how to write tests that confirm resources are removed correctly and that your API handles requests to delete non-existent items.