Testing Strategy
Relevant source files
The following files were used as context for generating this wiki page:
The project employs a comprehensive testing strategy using the Jest framework, divided into two primary categories: Unit Tests and End-to-End (E2E) Tests. This strategy ensures that individual components function correctly in isolation and that the API Gateway, as the primary entry point, integrates properly with its dependencies and handles various request scenarios.
The testing approach heavily relies on mocking to isolate units and simulate dependencies, such as database services, external clients, and other microservices. Tests are integrated into the CI/CD pipeline to automatically validate changes on every push and pull request, maintaining code quality and stability. The project aims for high test coverage, with tools available to generate coverage reports.
Sources: package.json:17-25, CI_CD_SETUP.md:10-14, AGENTS.md:30-32
Testing Framework and Configuration
The project uses Jest as its testing framework. The configuration is defined within the package.json file under the jest key. This central configuration applies to all test suites and defines important parameters like module resolution, test file patterns, and code transformation.
Sources: package.json:110-148
Jest Configuration
The following table outlines the key Jest configuration options used in the project.
| Property | Value | Description |
|---|---|---|
moduleFileExtensions | ["js", "json", "ts"] | Specifies the file extensions Jest will look for. |
rootDir | . | The root directory that Jest should scan for tests and modules. |
testRegex | .*\\.spec\\.ts$ | A regex pattern that Jest uses to detect test files. All files ending in .spec.ts are considered tests. |
transform | `{"^.+\.(t | j)s$": "ts-jest"}` |
coverageDirectory | ./coverage | The directory where test coverage reports will be generated. |
testEnvironment | node | Specifies that tests will run in a Node.js environment. |
roots | ["<rootDir>/apps/"] | A list of paths to directories that Jest should use to search for files. |
moduleNameMapper | (object) | Maps module paths to their actual locations, enabling aliased imports like @shared/ and @user-app/. |
Sources: package.json:111-147
Unit Testing
Unit tests are designed to test individual classes (services, controllers, builders, etc.) in isolation. The strategy involves creating a sandboxed NestJS module for each test suite using @nestjs/testing's Test.createTestingModule. All external dependencies are mocked to ensure the unit under test is not affected by external factors.
Sources: apps/product-app/src/products/products.service.spec.ts, apps/product-app/src/categories/categories.service.spec.ts
Mocking Strategy
Dependencies such as PrismaService, MinioService, and other providers are replaced with mock objects using jest.fn(). This allows testers to control the behavior of dependencies, simulate different scenarios (e.g., successful data retrieval, database errors), and assert that the correct methods were called with the expected arguments.
The diagram below illustrates the unit testing setup for a service, where all its dependencies are mocked.
This diagram shows a service being tested with all its dependencies replaced by mocks.
Sources: apps/product-app/src/products/products.service.spec.ts:15-40, apps/product-app/src/categories/categories.service.spec.ts:13-43
Example: Testing a Service
In service tests, the Test.createTestingModule is configured to provide mock implementations for all injected services.
// Source: apps/product-app/src/products/products.service.spec.ts:43-65
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ProductsService,
{
provide: PrismaService,
useValue: mockPrismaService,
},
{
provide: ProductQueryBuilder,
useValue: mockProductQueryBuilder,
},
{
provide: MinioService,
useValue: mockMinioService,
},
],
}).compile();
service = module.get<ProductsService>(ProductsService);
// ...
});
Tests cover both successful execution paths and error handling, such as verifying that an RpcException is thrown when a resource is not found.
// Source: apps/product-app/src/products/products.service.spec.ts:79-83
it('should throw RpcException when product not found', async () => {
mockPrismaService.product.findUnique.mockResolvedValue(null);
await expect(service.getById({ id: 'non-existent' })).rejects.toThrow(RpcException);
});
End-to-End (E2E) Testing
E2E tests focus on the API Gateway application. The goal is to test the gateway's controllers, guards, pipes, and error handling from the perspective of an HTTP client. Instead of communicating with actual microservices, the NATS client proxies (USER_SERVICE, PRODUCT_SERVICE, etc.) are mocked.
This approach allows for fast and reliable E2E tests that validate the gateway's behavior without the overhead and flakiness of a full multi-service deployment.
Sources: apps/gateway/test/gateway.e2e-spec.ts
E2E Test Flow
The typical E2E test involves sending an HTTP request using supertest to the gateway. The request passes through the gateway's middleware and guards. The JwtService is mocked to simulate authenticated users. When a controller attempts to send a message to a microservice, the call is intercepted by the mocked client, which returns a predefined response or error.
The following sequence diagram shows the E2E test flow for a protected endpoint.
This diagram illustrates how an E2E test validates the gateway by mocking the JWT service and the NATS microservice client.
Mocking Microservice Dependencies
In the E2E test setup, overrideProvider is used to replace the actual ClientProxy instances for each microservice with a mock object. This mock can then be controlled to return specific data or errors using rxjs observables like of() or throwError().
// Source: apps/gateway/test/gateway.e2e-spec.ts:26-31
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider('USER_SERVICE')
.useValue({
send: jest.fn(),
})
// ... other services
This setup is used to test various scenarios, including authentication, authorization, and error handling from downstream services.
// Source: apps/gateway/test/gateway.e2e-spec.ts:200-208
it('should handle microservice timeout errors', async () => {
// Giả lập Observable timeout
userService.send.mockReturnValue(throwError(() => ({ name: 'TimeoutError', message: 'Timeout' })));
await request(app.getHttpServer())
.get('/users/user-123')
.set('Authorization', `Bearer ${mockAccessToken}`)
.expect(408); // Request Timeout
});
Test Execution and Automation
The project provides several pnpm scripts to execute the different types of tests. These scripts are defined in package.json and are used for both local development and automated CI/CD pipelines.
Sources: package.json:17-29, SETUP.md:58-74, CI_CD_SETUP.md:12-14
Test Scripts
| Script | Description |
|---|---|
pnpm test | Runs all unit tests (.spec.ts) across all apps. Uses jest.unit.js config. |
pnpm test:watch | Runs unit tests in watch mode, re-running tests on file changes. |
pnpm test:cov | Runs unit tests and generates a code coverage report in the /coverage directory. |
pnpm test:e2e | Runs all end-to-end tests (.e2e-spec.ts). Uses jest.e2e.js config. |
pnpm test:full | Executes a full E2E test suite by first starting required services (databases) in Docker, running migrations, executing the tests, and then tearing down the containers. |
Sources: package.json:17-29
Full E2E Test Workflow
The test:full script provides a more integrated testing environment by using Docker Compose to spin up real database instances for the tests.
The workflow is as follows:
This flowchart details the steps executed by the pnpm test:full command.
Sources: package.json:28-29, SETUP.md:68-74
CI/CD Integration
Tests are a critical part of the CI/CD pipeline. The docker-build-test.yml workflow automatically runs tests for the gateway and user-app inside their respective Docker containers on every push or pull request to the main and develop branches. This ensures that no code that breaks existing functionality is merged.
Sources: CI_CD_SETUP.md:12-14, 53-59