SuperAPI AI Directory

domain-model-first

Testing & quality

View repository

Updated 10-15 22:39

About this skill

Domain Model-Centered Development Procedure

This guide explains principles that are not dependent on a specific programming language and can be applied in any language. Code examples are shown in TypeScript, but the concepts can be applied to other languages as well.

Test-First Domain Model Design and Implementation

Purpose

Implement pure domain logic that is not affected by external dependencies.

Specific Steps

  1. Define the behavior of the domain model as tests
    • Clarify the functions and constraints the model should have
    • Consider edge cases as well
  2. Implement the domain model to satisfy the tests
    • Implement according to the specifications indicated by the tests
    • Follow the design principles of value objects, entities, and aggregates
  3. Refine the design through refactoring
    • Eliminate code duplication
    • Clarify and separate responsibilities
    • Improve naming

Merit

  • Clarification of specifications and promotion of understanding of requirements
  • Improvement of design quality (separation of responsibilities and increased cohesion)
  • Ensuring the safety of refactoring
  • Early detection of edge cases
  • Value as documentation

Test Examples

// Taskのテスト例
describe("Task", () => {
  it("タスクが完了しているかどうかを確認できる", () => {
    const taskId = TaskId.of("task-1");
    const title = TaskTitle.of("テストタスク");

    const task = Task.of({
      id: taskId,
      title: title,
      completed: false,
      createdAt: new Date("2023-03-01T10:00:00")
    });

    expect(task.isCompleted()).toBe(false);

    const completedTask = task.markAsCompleted();
    expect(completedTask.isCompleted()).toBe(true);
  });

  it("期限日が未来の日付であることを検証できる", () => {
    const pastDate = new Date();
    pastDate.setDate(pastDate.getDate() - 1); // 昨日の日付

    const result = Task.validateDueDate(pastDate);

    expect(result.isFailure()).toBe(true);
    expect(result.error.message).toContain("期限は未来の日付である必要があります");
  });
});

Implementation of an In-Memory Repository

Purpose

  • Enable test execution of domain models and use cases without depending on a database

Specific Steps

  1. Definition of the repository interface
    • Define the operations required by the domain model
  2. Creation of the in-memory implementation
    • Use an in-memory collection
    • Simulate the behavior of an actual database

Use Case Development

Purpose

  • Implement application-layer logic using domain models and repositories

Specific Steps

  1. Define tests for the use case
    • Clarify execution conditions and expected results
    • Cover both success cases and failure cases
  2. Implement the use case to satisfy the tests
    • Use appropriate domain models and repositories
    • Apply business rules
  3. Refactoring
    • Separation and clarification of responsibilities

Test Examples

describe("CompleteTaskUseCase", () => {
  let taskRepository: TaskRepositoryInMemory;
  let completeTaskUseCase: CompleteTaskUseCase;

  beforeEach(() => {
    taskRepository = new TaskRepositoryInMemory();

    const task1 = Task.of({
      id: TaskId.of("task-1"),
      title: TaskTitle.of("未完了タスク"),
      completed: false,
      createdAt: new Date("2023-03-01T10:00:00"),
    });

    const task2 = Task.of({
      id: TaskId.of("task-2"),
      title: TaskTitle.of("既に完了しているタスク"),
      completed: true,
      createdAt: new Date("2023-03-01T13:00:00"),
    });

    taskRepository.save(task1);
    taskRepository.save(task2);

    completeTaskUseCase = new CompleteTaskUseCase(taskRepository);
  });

  it("タスクを完了できる", async () => {
    const result = await completeTaskUseCase.execute({
      taskId: "task-1",
    });

    expect(result.isSuccess()).toBe(true);

    const updatedTask = await taskRepository.findById(TaskId.of("task-1"));
    expect(updatedTask.isSuccess()).toBe(true);
    expect(updatedTask.value.isCompleted()).toBe(true);
  });

  it("存在しないタスクを処理できる", async () => {
    const result = await completeTaskUseCase.execute({
      taskId: "non-existent",
    });

    expect(result.isFailure()).toBe(true);
    expect(result.error.message).toContain("タスクが見つかりません");
  });
});

Implementation of Interface Adapters and Infrastructure

Purpose

  • Connect domain models and use cases to actual infrastructure

Specific Steps

  1. Implementation of the persistence repository

    • Connection to the actual database
    • Implementation of the repository interface
  2. Implementation of the controller

    • Validation of input and bridging to the use case
  3. Implementation of the presentation layer

    • Conversion of use case results into a display format

Integration testing and combined testing

Purpose

  • Verify the behavior of the entire system

Specific steps

  1. Creation of integration tests

    • Testing using actual infrastructure
    • End-to-end behavior verification
  2. Execution of combined tests

    • Verify collaboration between components

Risks of not following this development procedure

In design and implementation centered on the domain model, if the order shown in this knowledge (domain model → in-memory repository → use case → infrastructure) is not followed, the following serious risks arise:

  1. Distortion of domain model design

    • The domain model design is inappropriately affected by infrastructure or UI constraints
    • Entity design becomes influenced by the database schema
    • Compromise the domain model to match the constraints of the O/R mapper framework
  2. Leakage of technical concerns

    • Technical concerns such as persistence and transaction management become mixed into domain logic
    • The domain model becomes framework-dependent and deviates from pure business logic
  3. Increased difficulty of testing

    • Models containing many external dependencies make testing complex and reduce coverage
    • Slower test execution speed slows down the feedback cycle
  4. Reduced ease of change

    • Dependencies with external layers become complex, expanding the scope of impact of changes
    • Situations arise in which changes to the domain model force changes to the infrastructure
  5. Loss of conceptual consistency

    • Technical terms become mixed into the model instead of the domain experts' language (ubiquitous language)
    • The expression of business rules becomes unclear due to technical constraints

Summary: Advantages of this development approach

  1. The domain model design is not distorted

    • Pure domain design that is not dragged down by infrastructure constraints
    • Clear separation of business rules and technical concerns
  2. Early detection and easy correction of bugs

    • Tests clarify specifications and quickly detect the impact of changes
    • Unit-level test coverage increases
  3. Flexible response to requirement changes

    • A robust test foundation enables safe refactoring
    • Changes to domain logic do not depend on infrastructure
  4. Value as documentation

    • Tests document domain rules and behavior
    • Consistent use of ubiquitous language is promoted

Related skills (recommended for concurrent reading)

When using this skill, also refer to the following skills:

  • domain-building-blocks: Design of building blocks subject to TDD (value objects, entities, etc.)
  • aggregate-design: Aggregate design rules and boundary determination
  • repository-design: Repository design patterns, including in-memory repositories