DEV Community

Cover image for Sleeping Better at Night: Integration Tests and CI/CD for my Feedback API
Renato Silva
Renato Silva

Posted on

Sleeping Better at Night: Integration Tests and CI/CD for my Feedback API

In my last post, I implemented schema validation to protect my domain. But as the project grows, checking every route manually with Insomnia or Postman becomes a bottleneck.

Today, I automated that process using Vitest and Supertest, and integrated it into my CI/CD pipeline.

The Goal: Real Confidence

I wanted to ensure that when I call the /feedbacks endpoint:

  1. Valid data is correctly persisted in the database.
  2. Invalid data is blocked by my Zod schemas with a proper 400 Bad Request.

The Setup: Environment Isolation

One golden rule of testing is: Never test on your production database. I adjusted my SqliteFeedbackRepository to accept a database path, allowing me to use a dedicated database.test.db during test execution. Using cross-env, I flag the environment so the app knows which "mode" to run in.

// src/app.js
const databasePath = process.env.NODE_ENV === "test" 
  ? "./database.test.db" 
  : "./database.db";

const repository = new SqliteFeedbackRepository(databasePath);
Enter fullscreen mode Exit fullscreen mode

The Test Suite

Using Vitest, I created an E2E (End-to-End) test suite. It simulates real HTTP requests to the Fastify server and asserts the results.

// tests/submit-feedback.test.js
const { describe, it, expect, afterAll } = require('vitest');
const request = require('supertest');
const { app } = require('../app');

describe('Submit Feedback E2E', () => {
  it('should be able to submit a valid feedback', async () => {
    await app.ready();

    const response = await request(app.server)
      .post('/feedbacks')
      .send({
        name: "John Doe",
        email: "john@example.com",
        message: "This is a valid feedback message from integration test."
      });

    expect(response.status).toBe(201);
    expect(response.body).toHaveProperty('id');
  });

  it('should not be able to submit feedback with invalid email', async () => {
    await app.ready();

    const response = await request(app.server)
      .post('/feedbacks')
      .send({
        name: "John Doe",
        email: "invalid-email",
        message: "Valid message length"
      });

    expect(response.status).toBe(400);
  });
});
Enter fullscreen mode Exit fullscreen mode

Continuous Integration (CI) with Render

Testing on your local machine is great, but enforcing tests before deployment is better. I updated my **Render **configuration to include the test suite in the build process.

By changing the Build Command to:

npm install && npm test

I’ve created a safety net: If a single test fails, the build fails. The broken version will never reach the users, and the previous stable version stays online. This is the essence of Continuous Integration.

Challenges Overcome: The Windows File Lock

If you are developing on Windows, you might encounter the resource busy or locked error when trying to delete the SQLite test file after the tests. This happens because the process still holds a lock on the file.

The fix? Properly closing the Fastify instance and the repository connection:

afterAll(async () => {
  await app.close();
  // Ensure the database connection is closed before unlinking the file
});
Enter fullscreen mode Exit fullscreen mode

Why this matters

Now, every time I run git push, I get instant feedback. Automated tests are not a luxury; they are the foundation of professional software delivery. They give you the freedom to refactor and evolve your code without the fear of breaking what’s already working.


What's next? My API is now validated, tested, and protected by a CI/CD pipeline. To take it to the next level, I'll be looking into API Documentation with Swagger.

How do you handle testing in your deployment pipeline? Let's discuss in the comments!

Top comments (0)