How to Write Unit Tests for Use Cases in Clean Architecture
Unit tests for use cases are written by mocking the repository interface and injecting it into the use-case constructor, allowing you to verify business logic in isolation without external dependencies.
The castrozan/tcc repository demonstrates a clean-architecture pattern where use-case classes encapsulate core business logic and depend solely on abstractions such as repository interfaces. This design makes them exceptionally easy to unit test because you can replace real infrastructure concerns—like databases or HTTP clients—with lightweight mocks that return predefined data.
Why Use Cases Are Ideal for Unit Testing
Use-case classes in this architecture receive their dependencies through constructor injection. For example, CreateProfessionalUseCase accepts an implementation of IProfessionalRepository rather than importing a concrete database adapter. This inversion of control means:
- Tests run in milliseconds because they never touch the network or disk.
- You can simulate edge cases—like duplicate entries or database timeouts—by configuring the mock’s return values.
- The test suite remains stable regardless of external service availability.
Project Structure and Key Files
The repository organizes code into layers that separate concerns and facilitate testing:
| Layer | File Path | Responsibility |
|---|---|---|
| Domain Entity | src/domain/entities/professionals/Professional.ts |
Defines the core business object. |
| Repository Interface | src/domain/interfaces/professionals/IProfessionalRepository.ts |
Abstract contract that the use-case depends on. |
| DTO | src/application/dtos/professionals/CreateProfessionalDto.ts |
Input validation shape for the use-case. |
| Use Case | src/application/use-cases/professionals/CreateProfessionalUseCase.ts |
Orchestrates business rules using the repository abstraction. |
| Vitest Config | mcp-openapi-server/vitest.config.ts |
Test runner configuration for the monorepo. |
All test files follow the pattern *.test.ts or *.spec.ts and reside either next to the source code or under a __tests__/ directory.
Step-by-Step Guide to Writing Use Case Unit Tests
1. Create the Test File
Place the test module adjacent to the use-case implementation to keep imports short and context obvious. For the CreateProfessionalUseCase, the test path is:
professionals-dummy-app/src/application/use-cases/professionals/__tests__/CreateProfessionalUseCase.test.ts
2. Mock the Repository Interface
Use Vitest’s vi.fn() to create typed stubs for every method defined in IProfessionalRepository. This satisfies TypeScript’s strict checking while giving you spy capabilities to verify calls.
const mockRepo: IProfessionalRepository = {
create: vi.fn(),
findById: vi.fn(),
findAll: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
};
3. Instantiate the Use Case with the Mock
Inject the mock repository into the use-case constructor. This follows the same pattern used in production code, ensuring the test exercises the real instantiation logic.
const useCase = new CreateProfessionalUseCase(mockRepo);
4. Execute and Assert
Call the execute method with a valid DTO, then verify both the outcome and the interaction:
- Outcome: The returned value matches the mock’s resolved value.
- Interaction: The repository’s
createmethod received the exact DTO passed to the use-case.
Complete Code Example
The following test suite for CreateProfessionalUseCase demonstrates mocking, injection, and assertion patterns used throughout the repository:
// professionals-dummy-app/src/application/use-cases/professionals/__tests__/CreateProfessionalUseCase.test.ts
import { describe, it, expect, vi } from 'vitest';
import { CreateProfessionalUseCase } from '../../CreateProfessionalUseCase';
import { IProfessionalRepository } from '../../../../domain/interfaces/professionals/IProfessionalRepository';
import { CreateProfessionalDto } from '../../../dtos/professionals/CreateProfessionalDto';
import { Professional } from '../../../../domain/entities/professionals/Professional';
describe('CreateProfessionalUseCase', () => {
it('should forward the DTO to the repository and return the created entity', async () => {
// 1️⃣ Arrange – mock repository
const mockRepo: IProfessionalRepository = {
create: vi.fn(),
findById: vi.fn(),
findAll: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
};
const dto: CreateProfessionalDto = {
name: 'Ada Lovelace',
email: 'ada@example.com',
// …other required fields
};
const expectedProfessional: Professional = {
id: 'p-123',
...dto,
} as Professional; // cast for simplicity
// Stub the create method to resolve the expected entity
(mockRepo.create as any).mockResolvedValue(expectedProfessional);
// 2️⃣ Act – instantiate the use‑case with the mock and call execute
const useCase = new CreateProfessionalUseCase(mockRepo);
const result = await useCase.execute(dto);
// 3️⃣ Assert – repository was called correctly and result matches
expect(mockRepo.create).toHaveBeenCalledOnce();
expect(mockRepo.create).toHaveBeenCalledWith(dto);
expect(result).toEqual(expectedProfessional);
});
it('should throw an error when the DTO is missing', async () => {
const mockRepo: IProfessionalRepository = {
create: vi.fn(),
findById: vi.fn(),
findAll: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
};
const useCase = new CreateProfessionalUseCase(mockRepo);
// @ts-expect-error intentionally passing undefined
await expect(useCase.execute(undefined)).rejects.toThrow('Professional not found');
});
});
Key implementation details:
- The mock repository implements all methods of
IProfessionalRepositorybecause TypeScript requires a complete interface implementation. Unused methods can remain emptyvi.fn()stubs. mockResolvedValueconfigures the asynccreatemethod to return the fabricatedProfessionalentity, simulating a successful database insertion without an actual database.- The second test case demonstrates error-path testing by asserting that the use-case throws when receiving an undefined DTO, verifying validation logic inside the
executemethod.
Running the Tests
Execute the test suite using the npm script defined in the root package.json:
npm install # installs vitest and other dev dependencies
npm test # executes `vitest run`
Vitest discovers all *.test.ts files across the monorepo, including the use-case tests. Because the tests rely entirely on in-memory mocks, they complete in milliseconds and require no external services, databases, or Docker containers.
Summary
- Dependency injection via constructor parameters allows you to substitute real repositories with mocks, isolating the use-case from infrastructure concerns.
- Vitest provides the test runner and mocking utilities (
vi.fn(),mockResolvedValue) compatible with the repository’s TypeScript configuration. - Repository interfaces (
IProfessionalRepository) serve as the contract that mocks must fulfill, ensuring type safety throughout the test. - Arrange-Act-Assert pattern organizes tests into clear setup, execution, and verification phases that document the business rule being tested.
Frequently Asked Questions
What is a use case in clean architecture?
A use case is a class that encapsulates a specific business operation—such as creating a professional or updating equipment—by orchestrating domain entities and interacting with external systems only through abstract interfaces like repositories. In the castrozan/tcc repository, use cases reside in src/application/use-cases/ and contain the core logic that controllers invoke.
Why should I mock the repository instead of using a real database?
Mocking the repository keeps unit tests fast, deterministic, and isolated. Real databases introduce latency, require setup/teardown scripts, and can fail due to network issues or schema changes—none of which relate to the business logic inside the use case. By mocking IProfessionalRepository, you control exactly what data is returned and verify that the use case calls the correct methods with the correct arguments.
Can I use Jest instead of Vitest for these tests?
Yes, the patterns shown are compatible with Jest because Vitest’s API (describe, it, expect, vi.fn()) mirrors Jest’s. However, the castrozan/tcc repository specifically configures Vitest in mcp-openapi-server/vitest.config.ts to leverage native ES-module support and faster execution in a TypeScript monorepo. If you switch to Jest, ensure you configure ts-jest or babel-jest to handle TypeScript imports correctly.
How do I test error handling in use cases?
Test error paths by configuring the mock repository to reject with an error or by passing invalid input to the execute method, then assert that the use case throws the expected exception. For example, you can use mockRejectedValue to simulate a database failure, or pass undefined as the DTO and verify the use case throws 'Professional not found' using rejects.toThrow() as shown in the second test case of the example.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →