<aside> 💡

チャプター完了時点のソースコード

https://github.com/craftgear/api_handson/tree/ch05

</aside>

Applicationという単語が一般的すぎるので、ここでは代わりにUsecaseと呼ぶことにします。

  1. /src/todo/usecase.ts を作成

    import { TodoId, TodoRepository } from './domain';
    
    export const readTodos = (repository: TodoRepository, page = 1, limit = 10) => {
      // TODO: implement
    };
    
    export const createTodo = (repository: TodoRepository, title: string) => {
      // TODO: Implement
    };
    
    export const completeTodo = (repository: TodoRepository, id: TodoId) => {
      // TODO: Implement
    };
    
  2. /src/todo/domain.ts にリポジトリの型定義を追加

     export type TodoRepository = {
       selectAll: (offset: number, limit: number) => Promise<Todo[]>;
       selectById: (id: TodoId) => Promise<Todo | null>;
       insert: (todo: NewTodo) => Promise<Todo | null>;
       setCompleted: (id: TodoId) => Promise<Todo | null>;
     };
    
  3. 1で定義したユースケースの実装

    import {
      parseNewTodo,
      isComplete,
      TodoRepository,
      TodoId,
    } from './domain';
    
    export const readTodos = async ({ selectAll }: TodoRepository, page = 1, limit = 10) => {
      if (page < 1) {
        throw Error('page should be a positive number');
      }
      return await selectAll(page - 1, limit);
    };
    
    export const createTodo = async ({ insert }: TodoRepository, title: string) => {
      const todo = parseNewTodo({ title });
      return await insert(todo);
    };
    
    export const completeTodo = async (
      { selectById, setCompleted }: TodoRepository,
      id: TodoId
    ) => {
      const todo = await selectById(id);
      if (!todo) {
        throw new Error('Todo not found');
      }
      if (isComplete(todo)) {
        return todo;
      }
      return await setCompleted(id);
    };
    
  4. ユースケースのテストを追加 /src/todo/usecase.spec.ts

    import { describe, it, expect, vi } from 'vitest';
    import { readTodos, createTodo, completeTodo } from './usecase';
    import type { TodoRepository, TodoId } from './domain';
    
    describe.concurrent('todo usecases', () => {
      const repository: TodoRepository = {
        insert: vi.fn(),
        selectAll: vi.fn(),
        selectById: vi.fn(),
        setCompleted: vi.fn(),
      };
    
      afterEach(() => {
        vi.resetAllMocks();
      });
    
      it('reads todos', async () => {
        await readTodos(repository);
        expect(repository.selectAll).toHaveBeenCalledWith(0, 10);
      });
    
       it("page number should be larger then 0", () => {
         expect(() => readTodos(repository, 0)).rejects.toThrowError(
           "page should be a positive number",
         );
       });
       
       it('creates a new todo', async () => {
         const title = 'Buy milk';
         await createTodo(repository, title);
         expect(repository.insert).toHaveBeenCalledWith({ title, done: false });
       });
    
       it('completes a todo', async () => {
         const id = 1 as TodoId;
         const repo = {
            ...repository,
            selectById: vi.fn().mockResolvedValueOnce({
              id: 1 as TodoId,
              title: "Buy milk",
              done: false,
            }),
         };
         await completeTodo(repo, id);
         expect(repo.selectById).toHaveBeenCalledWith(id);
         expect(repo.setCompleted).toHaveBeenCalledWith(id);
       });
    
       it('does NOT completes a todo which is already done', async () => {
         const id = 2 as TodoId;
         const repo = {
           ...repository, 
           selectById: vi.fn().mockResolvedValueOnce({
             id: 2 as TodoId,
             title: "Buy eggs",
             done: true,
           }),
         };
         await completeTodo(repo, id);
         expect(repo.selectById).toHaveBeenCalledWith(id);
         expect(repo.setCompleted).not.toHaveBeenCalledWith(id);
       });
    
       it('throws an error when todo is not found', async () => {
         const id = 999 as TodoId;
         const repo = {
           ...repository,
           selectById: vi.fn().mockResolvedValueOnce(null),
         };
         expect(() => completeTodo(repo, id)).rejects.toThrowError("Todo not found");
       });
     });
    
  5. テストカバレッジを測定する