💊

DDDでテストコードの修正を楽にしたい。そのためにテスト用のドメインオブジェクトを生成する関数を作る

2022/09/16に公開

前提

  • NestJSでオニオンアーキテクチャーを採用しています。
  • ユーザー(user)が書籍(book)を借りたり、返却することができるサンプルを用意しています。
  • テストにはJestを利用しています。
  • テストの構造
    • ドメイン層:単体テスト
    • ユースケース層:単体テスト(リポジトリにはmockかinMemoryReposiotry使用)
    • リポジトリ層:結合テスト
    • コントローラー層:結合テスト(e2eテスト)

この記事で伝えたいこと

テスト用のドメインオブジェクトを生成する関数を最初に準備すると、開発の規模が大きくなってきたときに、テストコードの修正が用意になります。

解決したい課題

開発が進むにつれて、ドメインオブジェクトを変更した際、それを利用するテストコードの修正に大きな時間を要するようになってきました。
その結果、本来やりたいタスクをこなす時間が減ってしまったり、テストが蔑ろにされてしまうなどの懸念があり、どうにかしたいと思ったのがこの記事を書いた理由です。

課題の原因

原因は、なんと言ってもテスト用のドメインオブジェクトをベタ書きしていることです。
例えば以下の5つのユースケースがあるとします。

  • 利用者は書籍を借りることができる
  • 利用者は書籍を返却できる
  • 利用者は書籍を予約できる
  • 利用者は書籍の予約を取り消せる
  • 利用者は書籍の貸し出し期間を延長できる
    またそれに対応するコントローラーがあるとします。
    ですので、テストを書くときにbookのテスト用ドメインオブジェクトが、5(ユースケース層)+1(リポジトリ層)+5(コントローラー層)だけ最低でも必要になってきます。

課題を解決する技術、手法

以下の書籍を表すbookに対応するテスト用のドメインオブジェクト生成関数を用意します。

集約ルートのbook.ts

export interface IBook {
  name: string;
  author: string;
  bookSize: BookSize; // 定数系値オブジェクト
  borrower?: Borrower; // 普通の値オブジェクト
  reservationList: ReservationList; // 配列の値オブジェクト(ファーストクラスコレクション)
}

export class Book extends AggregateRoot<IBook, BookId> {
  public readonly name: IBook['name'];
  public readonly author: IBook['author'];
  public readonly bookSize: IBook['bookSize'];
  public readonly borrower: IBook['borrower'];
  public readonly reservationList: IBook['reservationList'];

  private constructor(props: IBook, id: BookId) {
    super(props, id);
    this.name = props.name;
    this.author = props.author;
    this.bookSize = props.bookSize;
    this.borrower = props.borrower;
    this.reservationList = props.reservationList;
  }

  public static construct(props: IBook): Book {
    return new Book(props, BookId.construct());
  }

  public static reConstruct(props: IBook, id: BookId): Book {
    return new Book(props, id);
  }

  bowwowBook() {
    // todo 今この書籍を誰も借りていないこと
    throw new Error('未実装');
  }

  returnBook() {
    // todo 検討、返却されたとき、返却日をどう入れる?
    throw new Error('未実装');
  }

  addReservation() {
    // todo 1人で複数回の予約は取れない
    throw new Error('未実装');
  }

  removeReservation() {
    throw new Error('未実装');
  }

  postpone() {
    // todo 予約がなければ延期できる
    throw new Error('未実装');
  }
}

テスト用のドメインオブジェクト生成関数
create-test-book.ts

const bookSizeKey = fetchRandomOne(Object.keys(BOOK_SIZE_TYPE));
export const createTestBook = (
  id: BookId = BookId.construct(),
  name: string = faker.name.fullName(),
  author: string = faker.name.fullName(),
  bookSize: BookSize = new BookSize({ value: BookSize[bookSizeKey] }),
  borrower: Borrower = undefined,
  reservationList: ReservationList = new ReservationList({ values: [] }),
): Book => {
  return Book.reConstruct(
    {
      name: name,
      author: author,
      bookSize: bookSize,
      borrower: borrower,
      reservationList: reservationList,
    },
    id,
  );
};

そのテスト
create-test-book.test.ts

describe('createTestBook', () => {
  it('値を何もしていしない場合', () => {
    // given:
    // when:
    const actual = createTestBook();
    // then:
    expect(actual).toStrictEqual(expect.any(Book));
  });

  it('値を付けたりつけなかった場合', () => {
    // given:
    const bookId = BookId.reConstruct('bookId');
    const author = 'author';
    const bookSize = new BookSize({ value: BOOK_SIZE_TYPE.bigSize });
    const borrower: Borrower = createTestBorrower(undefined, bookId);

    // when:
    const actual = createTestBook(
      bookId,
      undefined,
      author,
      bookSize,
      borrower,
      undefined,
    );

    // then:
    expect(actual).toEqual(expect.any(Book));
    expect(actual.id).toStrictEqual(bookId);
    expect(actual.author).toStrictEqual(author);
    expect(actual.bookSize.value).toStrictEqual(BOOK_SIZE_TYPE.bigSize);
    expect(actual.borrower).toStrictEqual(borrower);
    expect(actual.reservationList.values).toStrictEqual([]);
  });
});

定数系のドメインオブジェクトで使用するためのユーティリティ
配列の中から1つをランダムに取得する
fetch-random-one.ts

export const fetchRandomOne = <T>(values: T[]): T => {
  const number = Math.floor(Math.random() * values.length);
  return values[number];
};

課題がどう解決されるか

BOOKのドメインオブジェクトが変更されても、修正箇所が生成関数だけで済む場合があります。
もしくはテストコード内のドメインオブジェクト生成関数の引数の修正だけで済みます。

サンプル

ampersand-github/zenn-1

Discussion