1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Vitest】TypeScript / Expressにおけるドメイン駆動設計の各層別テスト実装 Part1: ドメイン層

Posted at

はじめに

この記事では、前回までのDDDシリーズで作成したアプリケーションにVitestを導入し、各層の自動テストを実装する方法について説明します。

DDDで設計したアプリケーションは、各層が独立しているためテストが書きやすいという特徴があります。その利点を活かして、実践的なテストコードを実装していきます。

この記事では、Vitestの利用設定とドメイン層のテストを実装します。

開発環境

開発環境は以下の通りです。

  • Windows11
  • Docker Engine 27.0.3
  • Docker Compose 2
  • PostgreSQL 18.1
  • Node.js 24.11.0
  • npm 11.6.2
  • TypeScript 5.9.3
  • Express 5.1.0
  • Prisma 6.18.0
  • Zod 4.1.12
  • Vitest 4.0.14

Vitestとは

Vitestは、Viteをベースにした高速なユニットテストフレームワークです。

特徴 説明
高速 Viteの高速なHMRを活用した爆速テスト実行
Jest互換 Jestと互換性のあるAPI
TypeScript対応 設定なしでTypeScriptをサポート
モック機能 強力なモック・スパイ機能を標準装備

Vitestのインストール

Vitest をインストールします。

npm install -D vitest

package.jsonの設定

package.jsonにテスト用のスクリプトを追加します。

package.json
{
  "scripts": {
    "test": "vitest"
  }
}

vitest.config.tsの作成

プロジェクトルートにvitest.config.tsを作成します。グローバル変数やエイリアスの設定を行います。

import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});

ドメイン層のテスト

ドメイン層は外部依存がないため、最もテストしやすい層です。値オブジェクトとエンティティの振る舞いや制約をテストします。

値オブジェクトのテスト

メールアドレスを表す値オブジェクトのテスト

メールアドレスの値オブジェクトのコンストラクタ(値の検証)と等価性チェックのテストを実装します。

__tests__/domain/value-objects/Email.test.ts
import { describe, it, expect } from "vitest";
import { Email } from "../../../src/domain/value-objects/Email";

describe("Email", () => {
  describe("constructor", () => {
    it("有効なメールアドレスでインスタンスを作成できる", () => {
      const email = new Email("test@example.com");
      expect(email.getValue()).toBe("test@example.com");
    });

    it("空文字列の場合はエラーをスローする", () => {
      expect(() => new Email("")).toThrow("Email is required");
    });
  });

  describe("equals", () => {
    it("同じ値のEmailオブジェクトはtrueを返す", () => {
      const email1 = new Email("test@example.com");
      const email2 = new Email("test@example.com");
      expect(email1.equals(email2)).toBe(true);
    });

    it("異なる値のEmailオブジェクトはfalseを返す", () => {
      const email1 = new Email("test1@example.com");
      const email2 = new Email("test2@example.com");
      expect(email1.equals(email2)).toBe(false);
    });
  });
});

ユーザー名を表す値オブジェクトのテスト

ユーザー名の値オブジェクトのコンストラクタ(値の検証)と等価性チェックのテストを実装します。

__tests__/domain/value-objects/UserName.test.ts
import { describe, it, expect } from "vitest";
import { UserName } from "../../../src/domain/value-objects/UserName";

describe("UserName", () => {
  describe("constructor", () => {
    it("有効な名前でインスタンスを作成できる", () => {
      const name = new UserName("John Doe");
      expect(name.getValue()).toBe("John Doe");
    });

    it("空文字列の場合はエラーをスローする", () => {
      expect(() => new UserName("")).toThrow("Name is required");
    });
  });

  describe("equals", () => {
    it("同じ値のUserNameオブジェクトはtrueを返す", () => {
      const name1 = new UserName("John Doe");
      const name2 = new UserName("John Doe");
      expect(name1.equals(name2)).toBe(true);
    });

    it("異なる値のUserNameオブジェクトはfalseを返す", () => {
      const name1 = new UserName("John Doe");
      const name2 = new UserName("Jane Doe");
      expect(name1.equals(name2)).toBe(false);
    });
  });
});

ユーザーIDを表す値オブジェクトの作成

ユーザーIDの値オブジェクトのコンストラクタ(正の数であることの検証)と等価性チェックのテストを実装します。

__tests__/domain/value-objects/UserId.test.ts
import { describe, expect, it } from "vitest";
import { UserId } from "../../../src/domain/value-objects/UserId";

describe("UserId", () => {
  describe("constructor", () => {
    it("有効なユーザーIDでインスタンスを作成できる", () => {
      const id = new UserId(1);
      expect(id.getValue()).toBe(1);
    });

    it("0の場合はエラーをスローする", () => {
      expect(() => new UserId(0)).toThrow("User ID must be a positive number");
    });
  });

  describe("equals", () => {
    it("同じ値のUserIdオブジェクトはtrueを返す", () => {
      const id1 = new UserId(1);
      const id2 = new UserId(1);
      expect(id1.equals(id2)).toBe(true);
    });

    it("異なる値のUserIdオブジェクトはfalseを返す", () => {
      const id1 = new UserId(1);
      const id2 = new UserId(2);
      expect(id1.equals(id2)).toBe(false);
    });
  });
});

エンティティのテスト

ユーザーエンティティのテスト

ユーザーエンティティの生成(create)、再構築(reconstruct)、属性変更(changeEmail, changeName)、およびプリミティブ型への変換(toObject)の振る舞いをテストします。

tests/domain/entities/User.test.ts
import { describe, it, expect } from "vitest";
import { User } from "../../../src/domain/entities/User";
import { Email } from "../../../src/domain/value-objects/Email";
import { UserName } from "../../../src/domain/value-objects/UserName";
import { UserId } from "../../../src/domain/value-objects/UserId";

describe("User", () => {
  describe("create", () => {
    it("新規ユーザーを作成できる", () => {
      const email = new Email("test@example.com");
      const name = new UserName("Test User");

      const user = User.create(email, name);

      expect(user.getEmail().getValue()).toBe("test@example.com");
      expect(user.getName().getValue()).toBe("Test User");
      expect(user.hasId()).toBe(false);
    });
  });

  describe("reconstruct", () => {
    it("既存ユーザーを再構築できる", () => {
      const id = new UserId(1);
      const email = new Email("test@example.com");
      const name = new UserName("Test User");

      const user = User.reconstruct(id, email, name);

      expect(user.getId()?.getValue()).toBe(1);
      expect(user.getEmail().getValue()).toBe("test@example.com");
      expect(user.getName().getValue()).toBe("Test User");
      expect(user.hasId()).toBe(true);
    });
  });

  describe("changeEmail", () => {
    it("メールアドレスを変更できる", () => {
      const id = new UserId(1);
      const email = new Email("old@example.com");
      const name = new UserName("Test User");
      const user = User.reconstruct(id, email, name);

      const newEmail = new Email("new@example.com");
      user.changeEmail(newEmail);

      expect(user.getEmail().getValue()).toBe("new@example.com");
    });

    it("同じメールアドレスへの変更はエラーをスローする", () => {
      const id = new UserId(1);
      const email = new Email("test@example.com");
      const name = new UserName("Test User");
      const user = User.reconstruct(id, email, name);

      const sameEmail = new Email("test@example.com");
      expect(() => user.changeEmail(sameEmail)).toThrow(
        "New email is the same as current email"
      );
    });
  });

  describe("changeName", () => {
    it("名前を変更できる", () => {
      const id = new UserId(1);
      const email = new Email("test@example.com");
      const name = new UserName("Old Name");
      const user = User.reconstruct(id, email, name);

      const newName = new UserName("New Name");
      user.changeName(newName);

      expect(user.getName().getValue()).toBe("New Name");
    });

    it("同じ名前への変更はエラーをスローする", () => {
      const id = new UserId(1);
      const email = new Email("test@example.com");
      const name = new UserName("Test User");
      const user = User.reconstruct(id, email, name);

      const sameName = new UserName("Test User");
      expect(() => user.changeName(sameName)).toThrow(
        "New name is the same as current name"
      );
    });
  });

  describe("toObject", () => {
    it("プリミティブ型に変換できる", () => {
      const id = new UserId(1);
      const email = new Email("test@example.com");
      const name = new UserName("Test User");
      const user = User.reconstruct(id, email, name);

      const obj = user.toObject();

      expect(obj).toEqual({
        id: 1,
        email: "test@example.com",
        name: "Test User",
      });
    });

    it("IDがない場合はエラーをスローする", () => {
      const email = new Email("test@example.com");
      const name = new UserName("Test User");
      const user = User.create(email, name);

      expect(() => user.toObject()).toThrow(
        "Cannot convert user without ID to object"
      );
    });
  });
});

まとめ

この記事では、ドメイン駆動設計のアプリケーションに Vitest を導入し、ドメイン層のテストを実装しました。
ドメイン駆動設計の利点(各層の独立性)を活かし、外部依存のないドメイン層(値オブジェクトとエンティティ)ドメイン駆動設計の具体的なテストコードを実装しました。各テストでは、値の検証、等価性のチェック、エンティティの生成/再構築、属性変更時の振る舞いなどが網羅されています。

次回は、アプリケーション層のテストを実装します。

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?