<aside> 💡
チャプター完了時点のソースコード
https://github.com/craftgear/api_handson/tree/ch05
</aside>
Applicationという単語が一般的すぎるので、ここでは代わりにUsecaseと呼ぶことにします。
/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
};
/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>;
};
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);
};
ユースケースのテストを追加 /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");
});
});
テストカバレッジを測定する