Strapi Testing — Unit Tests, Integration Tests, and API Testing
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
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.
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.
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.
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).
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
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.
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.
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.
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
Mini Project
Your task: Build a comprehensive test suite for a Strapi API.
- Set up Jest with a test database configuration (SQLite).
- Create a test article service with these methods:
validateArticleData— validates required fields and lengthsgetPublishedArticles— returns only published articlesgetArticleBySlug— finds article by URL slug
- 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
- Write integration tests for API endpoints:
- CRUD operations return correct status codes
- Missing fields return 400 errors
- Non-existent articles return 404
- Write a lifecycle hook test that verifies slug auto-generation.
- 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