# How to Write Unit Tests for Use Cases in Clean Architecture

> Learn how to write unit tests for use cases by mocking repositories and injecting them into your constructor. Isolate and verify business logic effectively.

- Repository: [Lucas Zanoni⠀⠀⠀⠀⠀ ⠀╱|、 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ (˚ˎ 。7 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ |、˜〵 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ じしˍ,)ノ/tcc](https://github.com/castrozan/tcc)
- Tags: tutorial
- Published: 2026-02-27

---

**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`](https://github.com/castrozan/tcc/blob/main/src/domain/entities/professionals/Professional.ts) | Defines the core business object. |
| **Repository Interface** | [`src/domain/interfaces/professionals/IProfessionalRepository.ts`](https://github.com/castrozan/tcc/blob/main/src/domain/interfaces/professionals/IProfessionalRepository.ts) | Abstract contract that the use-case depends on. |
| **DTO** | [`src/application/dtos/professionals/CreateProfessionalDto.ts`](https://github.com/castrozan/tcc/blob/main/src/application/dtos/professionals/CreateProfessionalDto.ts) | Input validation shape for the use-case. |
| **Use Case** | [`src/application/use-cases/professionals/CreateProfessionalUseCase.ts`](https://github.com/castrozan/tcc/blob/main/src/application/use-cases/professionals/CreateProfessionalUseCase.ts) | Orchestrates business rules using the repository abstraction. |
| **Vitest Config** | [`mcp-openapi-server/vitest.config.ts`](https://github.com/castrozan/tcc/blob/main/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.

```typescript
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.

```typescript
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 `create` method 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:

```typescript
// 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 `IProfessionalRepository` because TypeScript requires a complete interface implementation. Unused methods can remain empty `vi.fn()` stubs.
- `mockResolvedValue` configures the async `create` method to return the fabricated `Professional` entity, 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 `execute` method.

## Running the Tests

Execute the test suite using the npm script defined in the root [`package.json`](https://github.com/castrozan/tcc/blob/main/package.json):

```bash
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`](https://github.com/castrozan/tcc/blob/main/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.