Skip to content

Strapi Testing — Unit Tests, Integration Tests, and API Testing

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn how to test Strapi applications — writing unit tests for services and utilities, integration tests for controllers and lifecycle hooks, and automated API tests to verify endpoint behavior across changes.

What You'll Learn

  • How to set up a testing environment with Jest
  • How to write unit tests for Strapi services
  • How to write integration tests for controllers
  • How to test lifecycle hooks
  • How to write API endpoint tests using supertest
  • How to set up test fixtures and database cleanup

Why It Matters

Testing catches regressions before they reach production. When you upgrade Strapi, add a plugin, or refactor custom code, tests verify that everything still works. Without tests, every deployment is a leap of faith. With tests, you deploy with confidence.

Real-World Use

A Strapi project with 20 custom services and 15 lifecycle hooks had frequent regressions when developers made changes. After implementing a test suite with 150 tests, the team catches 95% of regressions before deployment. Deployment frequency increased from weekly to daily because the team trusts the test suite.

Learning Path

flowchart LR
  A["Strapi TypeScript"] --> B["Testing
-- You are here"]:::current B --> C["Production Setup"] C --> D["Database Configuration"] D --> E["Environment Variables"] classDef current fill:#4945ff,color:#fff,stroke-width:2px

Setting Up the Test Environment

# Install testing dependencies
npm install --save-dev jest @types/jest supertest @types/supertest
npm install --save-dev ts-jest  # If using TypeScript

# Create jest config
// jest.config.js
module.exports = {
  testEnvironment: "node",
  testMatch: ["**/tests/**/*.test.js"],
  setupFilesAfterSetup: ["./tests/setup.js"],
  testTimeout: 30000,  // Strapi startup can be slow
  verbose: true,
  forceExit: true,     // Force exit after tests
  detectOpenHandles: true,
};
// tests/setup.js — Test environment setup
const { setupStrapi, teardownStrapi } = require("./helpers/strapi");

beforeAll(async () => {
  await setupStrapi();
});

afterAll(async () => {
  await teardownStrapi();
});
// tests/helpers/strapi.js
const Strapi = require("@strapi/strapi");
const fs = require("fs");
const path = require("path");

let instance;

async function setupStrapi() {
  if (!instance) {
    // Use test database configuration
    process.env.NODE_ENV = "test";
    process.env.DATABASE_CLIENT = "sqlite";
    process.env.DATABASE_FILENAME = ".tmp/test.db";

    instance = await Strapi().load();

    // Clear test data before each test run
    await clearDatabase();
  }
  return instance;
}

async function teardownStrapi() {
  if (instance) {
    await instance.destroy();
    instance = null;
  }

  // Cleanup test database file
  const dbPath = path.join(__dirname, "../../.tmp/test.db");
  if (fs.existsSync(dbPath)) {
    fs.unlinkSync(dbPath);
  }
}

async function clearDatabase() {
  if (instance) {
    const models = Object.keys(instance.db.models);
    for (const model of models) {
      if (model.startsWith("api::") || model.startsWith("plugin::")) {
        await instance.db.query(model).deleteMany({});
      }
    }
  }
}

module.exports = { setupStrapi, teardownStrapi, clearDatabase };

Unit Testing Services

Unit tests verify that service methods work correctly in isolation.

// src/api/article/services/__tests__/article.test.js
const { setupStrapi, teardownStrapi } = require("../../../../../tests/helpers/strapi");

describe("Article Service", () => {
  let strapi;
  let articleService;

  beforeAll(async () => {
    strapi = await setupStrapi();
    articleService = strapi.service("api::article.article");
  });

  afterAll(async () => {
    await teardownStrapi();
  });

  describe("validateArticleData", () => {
    it("should return true for valid article data", async () => {
      const validData = {
        title: "Test Article",
        content: "This is test content",
      };

      const result = await articleService.validateArticleData(validData);
      expect(result).toBe(true);
    });

    it("should throw error for missing title", async () => {
      const invalidData = {
        content: "Content without title",
      };

      await expect(
        articleService.validateArticleData(invalidData)
      ).rejects.toThrow("Title is required");
    });

    it("should throw error for title exceeding max length", async () => {
      const invalidData = {
        title: "A".repeat(201),
        content: "Content",
      };

      await expect(
        articleService.validateArticleData(invalidData)
      ).rejects.toThrow("Title must be less than 200 characters");
    });
  });
});

Integration Testing Controllers

Integration tests verify that controllers handle requests correctly with the full Strapi stack.

// tests/api/article.test.js
const { setupStrapi, teardownStrapi } = require("../helpers/strapi");
const supertest = require("supertest");

let app;

beforeAll(async () => {
  const strapi = await setupStrapi();
  app = strapi.server.httpServer;
});

afterAll(async () => {
  await teardownStrapi();
});

describe("Article API", () => {
  describe("GET /api/articles", () => {
    it("should return empty array when no articles exist", async () => {
      const response = await supertest(app)
        .get("/api/articles")
        .expect(200);

      expect(response.body.data).toEqual([]);
      expect(response.body.meta.pagination.total).toBe(0);
    });

    it("should return articles when they exist", async () => {
      // Create a test article first
      await supertest(app)
        .post("/api/articles")
        .send({
          data: {
            title: "Test Article",
            content: "Test content",
          },
        })
        .expect(200);

      const response = await supertest(app)
        .get("/api/articles")
        .expect(200);

      expect(response.body.data.length).toBeGreaterThan(0);
      expect(response.body.data[0].attributes.title).toBe("Test Article");
    });
  });

  describe("POST /api/articles", () => {
    it("should create a new article", async () => {
      const response = await supertest(app)
        .post("/api/articles")
        .send({
          data: {
            title: "New Article",
            content: "Content for new article",
          },
        })
        .expect(200);

      expect(response.body.data.attributes.title).toBe("New Article");
      expect(response.body.data.attributes.content).toBe("Content for new article");
    });

    it("should return 400 for missing required fields", async () => {
      const response = await supertest(app)
        .post("/api/articles")
        .send({
          data: {
            // Missing title
            content: "Content only",
          },
        })
        .expect(400);

      expect(response.body.error).toBeDefined();
    });
  });

  describe("GET /api/articles/:id", () => {
    it("should return a single article", async () => {
      const createRes = await supertest(app)
        .post("/api/articles")
        .send({ data: { title: "Test", content: "Content" } });

      const articleId = createRes.body.data.id;

      const response = await supertest(app)
        .get(`/api/articles/${articleId}`)
        .expect(200);

      expect(response.body.data.id).toBe(articleId);
    });

    it("should return 404 for non-existent article", async () => {
      await supertest(app)
        .get("/api/articles/99999")
        .expect(404);
    });
  });
});

Testing Lifecycle Hooks

Test lifecycle hooks by creating/updating entries and verifying the side effects:

// tests/lifecycle/article-lifecycle.test.js
const { setupStrapi, teardownStrapi } = require("../helpers/strapi");

describe("Article Lifecycle Hooks", () => {
  let strapi;

  beforeAll(async () => {
    strapi = await setupStrapi();
  });

  afterAll(async () => {
    await teardownStrapi();
  });

  it("should auto-generate slug from title on creation", async () => {
    const article = await strapi.entityService.create("api::article.article", {
      data: {
        title: "My Test Article",
        content: "Content",
      },
    });

    expect(article.slug).toBeDefined();
    expect(article.slug).toBe("my-test-article");
  });

  it("should not overwrite custom slug", async () => {
    const article = await strapi.entityService.create("api::article.article", {
      data: {
        title: "My Test Article",
        slug: "custom-slug",
        content: "Content",
      },
    });

    expect(article.slug).toBe("custom-slug");
  });
});

Testing with Mock Data

For isolated tests that do not need a real database:

// tests/unit/service.test.js
const { createMockStrapi } = require("../helpers/mock-strapi");

// Mock the Strapi entity service
const mockEntityService = {
  create: jest.fn(),
  findMany: jest.fn(),
  findOne: jest.fn(),
  update: jest.fn(),
  delete: jest.fn(),
};

jest.mock("@strapi/strapi", () => ({
  factories: {
    createCoreService: jest.fn((uid, callback) => {
      return callback({ strapi: createMockStrapi() });
    }),
  },
}));

describe("Article Service (mocked)", () => {
  it("should call entity service with correct parameters", async () => {
    mockEntityService.create.mockResolvedValue({
      id: 1,
      title: "Test",
    });

    // Test service method
    const result = await articleService.create({
      data: { title: "Test", content: "Content" },
    });

    expect(mockEntityService.create).toHaveBeenCalledWith(
      "api::article.article",
      { data: { title: "Test", content: "Content" } }
    );
    expect(result.title).toBe("Test");
  });
});

Test Database Management

For integration tests, manage a separate test database:

// config/database.js — Test database configuration
module.exports = ({ env }) => {
  if (env("NODE_ENV") === "test") {
    return {
      connection: {
        client: "sqlite",
        connection: {
          filename: ".tmp/test.db",
        },
        useNullAsDefault: true,
      },
    };
  }

  // Production/development configuration
  return {
    connection: {
      client: env("DATABASE_CLIENT", "postgres"),
      connection: {
        host: env("DATABASE_HOST", "localhost"),
        port: env.int("DATABASE_PORT", 5432),
        database: env("DATABASE_NAME", "strapi"),
        user: env("DATABASE_USERNAME", "strapi"),
        password: env("DATABASE_PASSWORD", "password"),
      },
    },
  };
};

Running Tests

# Run all tests
npm test

# Run tests with coverage
npx jest --coverage

# Run specific test file
npx jest tests/api/article.test.js

# Run tests in watch mode (development)
npx jest --watch

Common Mistakes

  1. Using the production database for tests. Tests should never touch production data. Use a separate SQLite database or test database that is recreated for each test run.

  2. Not cleaning up between tests. Test data from one test can affect another. Clean up the database between tests or use transactions that roll back.

  3. Testing too much in a single test. Each test should verify one behavior. A test that creates an article, updates it, and deletes it is three separate tests. Keep tests focused.

  4. Mocking everything. Mocking is necessary for unit tests but integration tests should test the real system. Balance between unit tests (fast, isolated) and integration tests (real, comprehensive).

  5. Not testing error cases. Happy-path tests are important, but error cases (missing fields, invalid data, authentication failures) are where real bugs hide. Test both success and failure scenarios.

Practice Questions

  1. What tools are commonly used for testing Strapi applications? Answer: Jest (test runner) and Supertest (HTTP assertions). Jest provides test organization, assertions, and mocking. Supertest makes HTTP requests to the Strapi server.

  2. Why should tests use a separate SQLite database instead of the production database? Answer: Tests should be isolated and repeatable. A separate test database prevents data corruption, can be cleaned between tests, and avoids affecting real content.

  3. What is the difference between a unit test and an integration test in Strapi? Answer: Unit tests test individual functions in isolation (mocking dependencies). Integration tests test the full Strapi stack (real database, real controllers) to verify components work together.

  4. Challenge: Build a complete test suite for a Strapi project: (1) Set up Jest with a test database configuration, (2) Write unit tests for a service method (data validation, calculations), (3) Write integration tests for CRUD API endpoints, (4) Write lifecycle hook tests (slug generation, notification sending), (5) Test error cases (missing fields, invalid IDs, unauthorized access), (6) Achieve at least 80% Code Coverage.

FAQ

Can I run Strapi tests without a database?

No, Strapi requires a database. Use SQLite for test environments since it requires no external server. Configure process.env.DATABASE_CLIENT = 'sqlite' in your test setup.

How do I test authenticated endpoints?

Create a test user, log in to get a JWT, and include the JWT in the Authorization header of your test requests. Re-login if the token expires during long test runs.

Should I test Strapi core functionality?

No. Strapi core is tested by the Strapi team. Test your custom code — services, controllers, lifecycle hooks, middleware, and plugins.

How do I handle async test failures?

Use expect.assertions(number) to ensure the expected number of assertions run. This catches cases where an async test passes without actually running assertions because the promise rejected silently.

Can I run tests in parallel?

Yes, Jest supports parallel test execution. However, tests that share a database may interfere with each other. Use separate databases or test files that do not share state for parallel execution.

Mini Project

Your task: Build a comprehensive test suite for a Strapi API.

  1. Set up Jest with a test database configuration (SQLite).
  2. Create a test article service with these methods:
    • validateArticleData — validates required fields and lengths
    • getPublishedArticles — returns only published articles
    • getArticleBySlug — finds article by URL slug
  3. Write unit tests for each service method:
    • Valid data passes validation
    • Invalid data (missing title, too long) fails
    • Published articles filter works correctly
    • Slug lookup returns correct article
  4. Write integration tests for API endpoints:
    • CRUD operations return correct status codes
    • Missing fields return 400 errors
    • Non-existent articles return 404
  5. Write a lifecycle hook test that verifies slug auto-generation.
  6. Run the full suite and achieve 90%+ coverage on your custom code.

What's Next

Now that you understand testing, proceed to Production Setup to learn how to configure Strapi for production with PostgreSQL, Process management, and environment configuration. After that, explore Database Configuration.

Related lessons:

  • API Customization — Testing custom endpoints
  • Node.js Testing — General testing best practices
  • REST API — Testing API endpoints

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro