Help us understand the problem. What is going on with this article?

テスト駆動開発からはじめるドメイン駆動設計入門 ~値オブジェクト~

はじめに

この記事は書籍『ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本』で解説されているドメイン駆動設計パターンをテスト駆動開発で実装したものです。

言語はRubyです。Rubyでのテスト駆動開発の詳細に関してはこちらの記事をご参照ください。下のボタンをクリックするとブラウザ開発環境が起動するのでお手軽に開発を始めることが出来ます。

Open in Gitpod

値オブジェクト

ユーザーストーリー

まず ユーザーストーリー をもとに仕様を整理します。

ユーザーストーリーとは、ソフトウェアシステムに求められるふるまいをまとめたものだ。アジャイルソフトウェア開発の世界で広く使われており、大量の機能を細かく分解して計画作りに生かせるようにしている。
同じような概念を表す用語としてフィーチャーという言い方もあるが、
最近のアジャイル界隈では「ストーリー」とか「ユーザーストーリー」とかいう用語のほうが広まっている。

— Martin Fowler's Bliki (ja) https://bliki-ja.github.io/UserStory

SNS(ソーシャルネットワークサービス)のユーザー機能ということなので以下の ユーザーストーリー を作成しました。

利用者として
ユーザーを管理できるようにしたい
なぜならユーザーはシステムを利用するために必要だから

TODOリスト

ユーザーストーリー を作成したらそれをもとに TODOリスト を作成します。

TODOリスト はプログラムとして実行できる粒度で具体的に記述します。

  • ❏ ユーザーを管理できるようにする

    • ❏ ユーザーを登録する

      • ❏ IDと名前を持ったユーザーを作成する

仮実装

ユーザーを登録する

さっそく TODOリスト の1つ目を片付けましょう。

まずは テストファースト で最初に失敗するコードを書きます。

class HelloTest < Minitest::Test
  def test_greeting
    assert_equal 'hello world', greeting
  end
end

def greeting
  'hello world'
end

サンプルコードを以下のコードに書き換えてテストを実行します。

class UserTest < Minitest::Test
  def test_IDと名前を持ったユーザーを作成する
    user = User.new
    assert_equal '1', user.id
    assert_equal 'Bob', user.name
  end
end

テストは失敗しました。 NameError: uninitialized constant UserTest::User クラスが定義されていないからですね。

$ ruby test/hello_test.rb
Started with run options --seed 44125

UserTest
  test_IDと名前を持ったユーザーを作成する                                        ERROR (0.00s)
Minitest::UnexpectedError:         NameError: uninitialized constant UserTest::User
            test/hello_test.rb:7:in test_IDと名前を持ったユーザーを作成する


Finished in 0.00135s
1 tests, 0 assertions, 0 failures, 1 errors, 0 skips

テストをパスさせるためにUserクラスを追加します。

まずはテストをパスさせるために 仮実装 でベタ書きのコードを実装します。

class UserTest < Minitest::Test
  def test_IDと名前を持ったユーザーを作成する
    user = User.new
    assert_equal '1', user.id
    assert_equal 'Bob', user.name
  end
end

class User
  attr_accessor :id, :name

  def initialize
    @id = '1'
    @name = 'Bob'
  end
end

テストをパスさせてレッドからグリーンになりました。

$ ruby test/hello_test.rb
Started with run options --seed 55832

UserTest
  test_IDと名前を持ったユーザーを作成する                                         PASS (0.00s)

Finished in 0.00072s
1 tests, 2 assertions, 0 failures, 0 errors, 0 skips

仮実装から実装へ

ユーザーを登録する

テストは通りましたがコードはベタ書きのままです。

class UserTest < Minitest::Test
  def test_IDと名前を持ったユーザーを作成する
    user = User.new
    assert_equal '1', user.id
    assert_equal 'Bob', user.name
  end
end

class User
  attr_accessor :id, :name

  def initialize
    @id = '1'
    @name = 'Bob'
  end
end

仮実装 のままでは別のユーザーを作ることが出来ないので、コンストラクタ経由で作成できるようにします。

class UserTest < Minitest::Test
  def test_IDと名前を持ったユーザーを作成する
    user = User.new('1', 'Bob')
    assert_equal '1', user.id
    assert_equal 'Bob', user.name
  end
end

class User
  attr_accessor :id, :name

  def initialize(id, name)
    @id = id
    @name = name
  end
end

テストが通りました。

$ ruby test/hello_test.rb
Started with run options --seed 6402

UserTest
  test_IDと名前を持ったユーザーを作成する                                         PASS (0.00s)

Finished in 0.00089s
1 tests, 2 assertions, 0 failures, 0 errors, 0 skips

仮実装から実装へ を経て一つ目の TODOリスト を片付けたのでここでバージョン管理システムを使ってコミットしておきます。

$ git add .
$ git commit -m 'test: ユーザーを登録する'

以下、 TODOリスト を片付けるたびにコミットしていきます。

リファクタリング

  • ❏ ユーザーを管理できるようにする

    • ❏ ユーザーを登録する

      • ✓ IDと名前を持ったユーザーを作成する

レッド・グリーンときたので続いて リファクタリング を実施します。

メソッドの抽出

本来はコードの重複が発生してからやるのですが今回は予め メソッドの抽出 を適用してフィクスチャーを抽出しておきます。

class UserTest < Minitest::Test
  def test_IDと名前を持ったユーザーを作成する
    user = User.new('1', 'Bob')
    assert_equal '1', user.id
    assert_equal 'Bob', user.name
  end
end

Rubyのテスティングフレームワークminitestではフィクスチャーはsetupメソッドです。

class UserTest < Minitest::Test
  def setup
    @user = User.new('1', 'Bob')
  end

  def test_IDと名前を持ったユーザーを作成する
    assert_equal '1', @user.id
    assert_equal 'Bob', @user.name
  end
end

テストが壊れていないことを確認したらコミットします。

明白な実装

続いて TODOリスト を追加します。

  • ❏ ユーザーを管理できるようにする

    • ❏ ユーザーを登録する

      • ✓ IDと名前を持ったユーザーを作成する
      • ユーザー名が3文字未満の場合はエラー

ユーザーを登録する

追加した TODOリスト に取り掛かります。

class UserTest < Minitest::Test
  def setup
    @user = User.new('1', 'Bob')
  end

  def test_IDと名前を持ったユーザーを作成する
    assert_equal '1', @user.id
    assert_equal 'Bob', @user.name
  end
end

class User
  attr_accessor :id, :name

  def initialize(id, name)
    @id = id
    @name = name
  end
end

まず、失敗するテストを書いて 明白な実装 でテストをパスするようにします。

class UserTest < Minitest::Test
  def setup
    @user = User.new('1', 'Bob')
  end

  def test_IDと名前を持ったユーザーを作成する
    assert_equal '1', @user.id
    assert_equal 'Bob', @user.name
  end

  def test_ユーザー名が3文字未満の場合はエラー
    e = assert_raises RuntimeError do
      User.new('1', 'a')
    end

    assert_equal 'ユーザー名は3文字以上です。', e.message
  end
end

class User
  attr_accessor :id, :name

  def initialize(id, name)
    raise 'ユーザー名は3文字以上です。' if name.length < 3

    @id = id
    @name = name
  end
end

レッドからグリーンになったことを確認したらコミットします。

リファクタリング

  • ❏ ユーザーを管理できるようにする

    • ❏ ユーザーを登録する

      • ✓ IDと名前を持ったユーザーを作成する
      • ✓ ユーザー名が3文字未満の場合はエラー

レッド・グリーン・リファクタリングです。

クラスの抽出

クラスの抽出 を適用して Userクラスから 値オブジェクト を抽出する リファクタリング を適用します。

class UserTest < Minitest::Test
  def setup
    @user = User.new('1', 'Bob')
  end

  def test_IDと名前を持ったユーザーを作成する
    assert_equal '1', @user.id
    assert_equal 'Bob', @user.name
  end

  def test_ユーザー名が3文字未満で新規登録する場合はエラー
    e = assert_raises RuntimeError do
      User.new(1, 'a')
    end

    assert_equal 'ユーザー名は3文字以上です。', e.message
  end
end

class User
  attr_accessor :id, :name

  def initialize(id, name)
    raise 'ユーザー名は3文字以上です。' if name.length < 3

    @id = id
    @name = name
  end
end

まずはUserIdクラスを抽出します。テストコードをUserIdクラスを使って呼び出すように変更したらエラーを修正してグリーンの状態を維持します。

class UserTest < Minitest::Test
  def setup
    id = UserId.new('1')
    @user = User.new(id, 'Bob')
  end

  def test_IDと名前を持ったユーザーを作成する
    assert_equal '1', @user.id.value
    assert_equal 'Bob', @user.name
  end
...
end

class UserId
  attr_accessor :value

  def initialize(value)
    @value = value
  end
end

...

続いてUserNameクラスを抽出します。テストコードも同様に変更します。

class UserTest < Minitest::Test
  def setup
    id = UserId.new('1')
    name = UserName.new('Bob')
    @user = User.new(id, name)
  end

  def test_IDと名前を持ったユーザーを作成する
    assert_equal '1', @user.id.value
    assert_equal 'Bob', @user.name.value
  end
...
end

...

class UserName
  attr_accessor :value

  def initialize(value)
    raise 'ユーザー名は3文字以上です。' if value.length < 3

    @value = value
  end
end

...

テストコードを修正してグリーンになったらコミットして クラスの抽出リファクタリング 完了です。

$ruby test/hello_test.rb
Started with run options --seed 59746

UserTest
  test_ユーザー名が3文字未満の場合はエラー                                         PASS (0.00s)
  test_IDと名前を持ったユーザーを作成する                                         PASS (0.00s)

Finished in 0.00071s
2 tests, 4 assertions, 0 failures, 0 errors, 0 skips

setメソッドの削除

クラスの抽出 により 値オブジェクト を抽出することは出来ましたがインスタンスの値が変更可能な状態です。

setメソッドの削除 を適用して 値オブジェクト の要求を満たす不変オブジェクトに リファクタリング しましょう。

class UserTest < Minitest::Test
  def setup
    id = UserId.new('1')
    name = UserName.new('Bob')
    @user = User.new(id, name)
  end

  def test_IDと名前を持ったユーザーを作成する
    assert_equal '1', @user.id.value
    assert_equal 'Bob', @user.name.value
  end

  def test_ユーザー名が3文字未満の場合はエラー
    e = assert_raises RuntimeError do
      UserName.new('a')
    end

    assert_equal 'ユーザー名は3文字以上です。', e.message
  end
end

class UserId
  attr_accessor :value

  def initialize(value)
    @value = value
  end
end

class UserName
  attr_accessor :value

  def initialize(value)
    raise 'ユーザー名は3文字以上です。' if value.length < 3

    @value = value
  end
end

class User
  attr_accessor :id, :name

  def initialize(id, name)
    @id = id
    @name = name
  end
end

アックセッサメソッドを読み取り専用に変更します。

...

class UserId
  attr_reader :value

...
end

class UserName
  attr_reader :value

...
end

class User
  attr_reader :id, :name

...
end

テストが壊れていないことを確認したらコミットします。

$ ruby test/hello_test.rb
Started with run options --seed 62273

UserTest
  test_ユーザー名が3文字未満の場合はエラー                                         PASS (0.00s)
  test_IDと名前を持ったユーザーを作成する                                         PASS (0.00s)

Finished in 0.00075s
2 tests, 4 assertions, 0 failures, 0 errors, 0 skips

例外ケース

正常系の実装が出来たので続いて例外系の実装に入りたいと思います。

まず TODOリスト を追加します。

  • ❏ ユーザーを管理できるようにする

    • ❏ ユーザーを登録する

      • ✓ IDと名前を持ったユーザーを作成する
      • ✓ ユーザー名が3文字未満の場合はエラー
      • ユーザー名が4文字の場合は登録される
      • ユーザー名を指定しない場合はエラー
      • IDを指定しない場合はエラー

ユーザーを登録する

追加した TODOリスト をテストを壊さないように1つづつ片付けていくとしましょう。

class UserTest < Minitest::Test
  def setup
    id = UserId.new('1')
    name = UserName.new('Bob')
    @user = User.new(id, name)
  end

  def test_IDと名前を持ったユーザーを作成する
    assert_equal '1', @user.id.value
    assert_equal 'Bob', @user.name.value
  end

  def test_ユーザー名が3文字未満の場合はエラー
    e = assert_raises RuntimeError do
      UserName.new('a')
    end

    assert_equal 'ユーザー名は3文字以上です。', e.message
  end
end

class UserId
  attr_reader :value

  def initialize(value)
    @value = value
  end
end

class UserName
  attr_reader :value

  def initialize(value)
    raise 'ユーザー名は3文字以上です。' if value.length < 3

    @value = value
  end
end

class User
  attr_reader :id, :name

  def initialize(id, name)
    @id = id
    @name = name
  end
end

実装後のコードです。

実際は1つテストコードを追加したらプロダクトコードを実装してレッド・グリーンのサイクルを回しています。

class UserTest < Minitest::Test
  def setup
    id = UserId.new('1')
    name = UserName.new('Bob')
    @user = User.new(id, name)
  end

  def test_IDと名前を持ったユーザーを作成する
    assert_equal '1', @user.id.value
    assert_equal 'Bob', @user.name.value
  end

  def test_ユーザー名が3文字未満の場合はエラー
    e = assert_raises RuntimeError do
      UserName.new('a')
    end

    assert_equal 'ユーザー名は3文字以上です。', e.message
  end

  def test_ユーザー名が4文字の場合は登録される
    user = User.new(UserId.new('1'), UserName.new('abcd'))
    assert_equal 'abcd', user.name.value
  end

  def test_ユーザー名を指定しない場合はエラー
    assert_raises RuntimeError do
      UserName.new(nil)
    end
  end

  def test_IDを指定しない場合はエラー
    assert_raises RuntimeError do
      UserId.new(nil)
    end
  end
end

class UserId
  attr_reader :value

  def initialize(value)
    raise if value.nil?

    @value = value
  end
end

class UserName
  attr_reader :value

  def initialize(value)
    raise if value.nil?
    raise 'ユーザー名は3文字以上です。' if value.length < 3

    @value = value
  end
end

class User
  attr_reader :id, :name

  def initialize(id, name)
    @id = id
    @name = name
  end
end

例外系の機能を追加してテストもパスしたのでコミットします。

リファクタリング

  • ❏ ユーザーを管理できるようにする

    • ✓ ユーザーを登録する

      • ✓ IDと名前を持ったユーザーを作成する
      • ✓ ユーザー名が3文字未満の場合はエラー
      • ✓ ユーザー名を指定しない場合はエラー
      • ✓ ユーザー名が4文字の場合は登録される
      • ✓ IDを指定しない場合はエラー

今回はコードの可読性を改善する観点で リファクタリング を実施してみたいと思います。

メソッドのインライン化

テストコードが増えてきましたここでテストコードをグルーピングするため メソッドのインライン化 を適用します。

class UserTest < Minitest::Test
  def setup
    id = UserId.new('1')
    name = UserName.new('Bob')
    @user = User.new(id, name)
  end

  def test_IDと名前を持ったユーザーを作成する
    assert_equal '1', @user.id.value
    assert_equal 'Bob', @user.name.value
  end

  def test_ユーザー名が3文字未満の場合はエラー
    e = assert_raises RuntimeError do
      UserName.new('a')
    end

    assert_equal 'ユーザー名は3文字以上です。', e.message
  end

  def test_ユーザー名が4文字の場合は登録される
    user = User.new(UserId.new('1'), UserName.new('abcd'))
    assert_equal 'abcd', user.name.value
  end

  def test_ユーザー名を指定しない場合はエラー
    assert_raises RuntimeError do
      UserName.new(nil)
    end
  end

  def test_IDを指定しない場合はエラー
    assert_raises RuntimeError do
      UserId.new(nil)
    end
  end
end

class UserId
  attr_reader :value

  def initialize(value)
    raise if value.nil?

    @value = value
  end
end

class UserName
  attr_reader :value

  def initialize(value)
    raise if value.nil?
    raise 'ユーザー名は3文字以上です。' if value.length < 3

    @value = value
  end
end

class User
  attr_reader :id, :name

  def initialize(id, name)
    @id = id
    @name = name
  end
end

テストコードの構造を TODOリスト の構造に合わせることで可読性を改善します。

class UserTest < Minitest::Test
  describe 'ユーザーを登録する' do
    def setup
      id = UserId.new('1')
      name = UserName.new('Bob')
      @user = User.new(id, name)
    end

    def test_IDと名前を持ったユーザーを作成する
      assert_equal '1', @user.id.value
      assert_equal 'Bob', @user.name.value
    end

    def test_ユーザー名が3文字未満の場合はエラー
      e = assert_raises RuntimeError do
        UserName.new('a')
      end

      assert_equal 'ユーザー名は3文字以上です。', e.message
    end

    def test_ユーザー名が4文字の場合は登録される
      user = User.new(UserId.new('1'), UserName.new('abcd'))
      assert_equal 'abcd', user.name.value
    end

    def test_ユーザー名を指定しない場合はエラー
      assert_raises RuntimeError do
        UserName.new(nil)
      end
    end

    def test_IDを指定しない場合はエラー
      assert_raises RuntimeError do
        UserId.new(nil)
      end
    end
  end
end

...

実行結果もわかりやすいなりました。 テストは壊れていないのでコミットします。

$ ruby test/hello_test.rb
Started with run options --seed 39340

ユーザーを登録する
  test_IDと名前を持ったユーザーを作成する                                         PASS (0.00s)
  test_IDを指定しない場合はエラー                                             PASS (0.00s)
  test_ユーザー名が3文字未満の場合はエラー                                         PASS (0.00s)
  test_ユーザー名を指定しない場合はエラー                                          PASS (0.00s)
  test_ユーザー名が4文字の場合は登録される                                         PASS (0.00s)

Finished in 0.00217s
5 tests, 7 assertions, 0 failures, 0 errors, 0 skips

キーワード引数の導入

テストコードは読みやすくなりました。続いてプロダクトコードを改善しましょう。

動的言語であるRubyでは型を明示しないため引数の値がリテラルなのか 値オブジェクト なのかメソッドの定義だけでは把握できません。キーワード引数の導入 をしてできるだけ引数の型を把握しやすいように リファクタリング しましょう。

class UserTest < Minitest::Test
  describe 'ユーザーを登録する' do
    def setup
      id = UserId.new('1')
      name = UserName.new('Bob')
      @user = User.new(id, name)
    end

    def test_IDと名前を持ったユーザーを作成する
      assert_equal '1', @user.id.value
      assert_equal 'Bob', @user.name.value
    end

    def test_ユーザー名が3文字未満の場合はエラー
      e = assert_raises RuntimeError do
        UserName.new('a')
      end

      assert_equal 'ユーザー名は3文字以上です。', e.message
    end

    def test_ユーザー名が4文字の場合は登録される
      user = User.new(UserId.new('1'), UserName.new('abcd'))
      assert_equal 'abcd', user.name.value
    end

    def test_ユーザー名を指定しない場合はエラー
      assert_raises RuntimeError do
        UserName.new(nil)
      end
    end

    def test_IDを指定しない場合はエラー
      assert_raises RuntimeError do
        UserId.new(nil)
      end
    end
  end
end

class UserId
  attr_reader :value

  def initialize(value)
    raise if value.nil?

    @value = value
  end
end

class UserName
  attr_reader :value

  def initialize(value)
    raise if value.nil?
    raise 'ユーザー名は3文字以上です。' if value.length < 3

    @value = value
  end
end

class User
  attr_reader :id, :name

  def initialize(id, name)
    @id = id
    @name = name
  end
end

キーワード引数値オブジェクト と同じ名称にします。

class UserTest < Minitest::Test
  describe 'ユーザーを登録する' do
    def setup
      id = UserId.new('1')
      name = UserName.new('Bob')
      @user = User.new(user_id: id, user_name: name)
    end
...
end

...

class User
  attr_reader :id, :name

  def initialize(user_id:, user_name:)
    @id = user_id
    @name = user_name
  end
end

テストを修正してグリーンになったらコミットします。

ちなみに キーワード引数の導入 という名称はリファクタリングのカタログにはない用語です。Ruby固有のパターンとして便宜上命名しています。

モジュール分割

TODOリスト を全部片付けたのでここで単一ファイルから各クラスモジュールごとに モジュール分割 を実施します。

TODOリスト

  • ❏ ユーザーを管理できるようにする

    • ✓ ユーザーを登録する

      • ✓ IDと名前を持ったユーザーを作成する
      • ✓ ユーザー名が3文字未満の場合はエラー
      • ✓ ユーザー名を指定しない場合はエラー
      • ✓ ユーザー名が4文字の場合は登録される
      • ✓ IDを指定しない場合はエラー

クラス図

diag-3a0995bbcfccf7896acca758266aea29.png

ファイル構成

/main.rb
  |--lib/
      |
       -- sns.rb
       -- user_id.rb
       -- user_name.rb
       -- user.rb
  |--test/
      |
       -- user_test.rb

/main.rb.

require './test/user_test.rb'

/lib/sns.rb.

# frozen_string_literal: true

require './lib/user_id.rb'
require './lib/user_name.rb'
require './lib/user.rb'

/lib/user_id.rb.

# frozen_string_literal: true

# User ID value object
class UserId
  attr_reader :value

  def initialize(value)
    raise if value.nil?

    @value = value
  end
end

/lib/user_name.rb.

# frozen_string_literal: true

# User name value object
class UserName
  attr_reader :value

  def initialize(value)
    raise if value.nil?
    raise 'ユーザー名は3文字以上です。' if value.length < 3

    @value = value
  end
end

/lib/user.rb.

# frozen_string_literal: true

# User
class User
  attr_reader :id, :name

  def initialize(user_id:, user_name:)
    @id = user_id
    @name = user_name
  end
end

/test/user_test.rb.

# frozen_string_literal: true

require 'minitest/reporters'
Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new(color: true)]
require 'minitest/autorun'
require './lib/sns.rb'

class UserTest < Minitest::Test
  describe 'ユーザーを登録する' do
    def setup
      id = UserId.new('1')
      name = UserName.new('Bob')
      @user = User.new(user_id: id, user_name: name)
    end

    def test_IDと名前を持ったユーザーを作成する
      assert_equal '1', @user.id.value
      assert_equal 'Bob', @user.name.value
    end

    def test_ユーザー名が3文字未満の場合はエラー
      e = assert_raises RuntimeError do
        UserName.new('a')
      end

      assert_equal 'ユーザー名は3文字以上です。', e.message
    end

    def test_ユーザー名が4文字の場合は登録される
      user = User.new(user_id: UserId.new('1'),
                      user_name: UserName.new('abcd'))
      assert_equal 'abcd', user.name.value
    end

    def test_ユーザー名を指定しない場合はエラー
      assert_raises RuntimeError do
        UserName.new(nil)
      end
    end

    def test_IDを指定しない場合はエラー
      assert_raises RuntimeError do
        UserId.new(nil)
      end
    end
  end
end

リリース

モジュール分割 により最初のリリースの準備が出来ました。

リリース前に 静的コード解析コードカバレッジ を実施してコードの品質を確認しておきましょう。

手順の詳細は こちらの記事 をご参照ください。

静的コード解析

$ rubocop
Inspecting 5 files
....C

Offenses:

test/user_test.rb:11:3: C: Metrics/BlockLength: Block has too many lines. [30/25]
  describe 'ユーザーを登録する' do ...
  ^^^^^^^^^^^^^^^^^^^^^^^
test/user_test.rb:18:9: C: Naming/MethodName: Use snake_case for method names.
    def test_IDと名前を持ったユーザーを作成する
        ^^^^^^^^^^^^^^^^^^^^^^^
test/user_test.rb:18:16: C: Naming/AsciiIdentifiers: Use only ascii symbols in identifiers.
    def test_IDと名前を持ったユーザーを作成する
               ^^^^^^^^^^^^^^^^
test/user_test.rb:23:9: C: Naming/MethodName: Use snake_case for method names.
    def test_ユーザー名が3文字未満の場合はエラー
        ^^^^^^^^^^^^^^^^^^^^^^^
test/user_test.rb:23:14: C: Naming/AsciiIdentifiers: Use only ascii symbols in identifiers.
    def test_ユーザー名が3文字未満の場合はエラー
             ^^^^^^^^^^^^^^^^^^
test/user_test.rb:31:9: C: Naming/MethodName: Use snake_case for method names.
    def test_ユーザー名が4文字の場合は登録される
        ^^^^^^^^^^^^^^^^^^^^^^^
test/user_test.rb:31:14: C: Naming/AsciiIdentifiers: Use only ascii symbols in identifiers.
    def test_ユーザー名が4文字の場合は登録される
             ^^^^^^^^^^^^^^^^^^
test/user_test.rb:37:9: C: Naming/MethodName: Use snake_case for method names.
    def test_ユーザー名を指定しない場合はエラー
        ^^^^^^^^^^^^^^^^^^^^^^
test/user_test.rb:37:14: C: Naming/AsciiIdentifiers: Use only ascii symbols in identifiers.
    def test_ユーザー名を指定しない場合はエラー
             ^^^^^^^^^^^^^^^^^
test/user_test.rb:43:9: C: Naming/MethodName: Use snake_case for method names.
    def test_IDを指定しない場合はエラー
        ^^^^^^^^^^^^^^^^^^^
test/user_test.rb:43:16: C: Naming/AsciiIdentifiers: Use only ascii symbols in identifiers.
    def test_IDを指定しない場合はエラー
               ^^^^^^^^^^^^

5 files inspected, 11 offenses detected

いくつか警告が表示されていますがテストコードの日本語に関する内容なのでチェック対象から除外することにします。

.rubocop.yml ファイルを以下に更新します。

inherit_from: .rubocop_todo.yml

AllCops:
    Include:
      - 'lib/**/*.rb'
      - 'test/**/*_test.rb'
    Exclude:
      - 'docs'

Style/AsciiComments:
  Enabled: false

Naming/MethodName:
  Exclude:
    - 'test/**'

Naming/AsciiIdentifiers:
  Exclude:
    - 'test/**'

Metrics/BlockLength:
  Exclude:
    - 'test/**'

警告は無くなりました。

$ rubocop
Inspecting 5 files
.....

5 files inspected, no offenses detected

コードカバレッジ

まず、テストコードからコードカバレッジを実行できるようにします。

user_test.rb の先頭を以下に更新します。

# frozen_string_literal: true

require 'simplecov'
SimpleCov.start
require 'minitest/reporters'
Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new(color: true)]
require 'minitest/autorun'
require './lib/sns.rb'

...

テストを実行します。

$ ruby test/user_test.rb
Started with run options --seed 24571

ユーザーを登録する
  test_IDを指定しない場合はエラー                                             PASS (0.00s)
  test_ユーザー名が4文字の場合は登録される                                         PASS (0.00s)
  test_ユーザー名を指定しない場合はエラー                                          PASS (0.00s)
  test_ユーザー名が3文字未満の場合はエラー                                         PASS (0.00s)
  test_IDと名前を持ったユーザーを作成する                                         PASS (0.00s)

Finished in 0.00106s
5 tests, 7 assertions, 0 failures, 0 errors, 0 skips
Coverage report generated for Unit Tests to /Users/k2works/Projects/sandbox/tdd_itddd/coverage. 19 / 19 LOC (100.0%) covered.

テストカバレッジは100%です。

20200222.png

ふりかえり

最初のリリースが完了したのでここでやってきたことのふりかえりをしておきましょう。

まず、ユーザーストーリー から TODOリスト を作成しました。
TODOリスト の1つめを 仮実装 でまずベタ書きのコードを書いてテストをパスするようにしました。
テストをパスしてグリーンになったら 仮実装から実装へ を経て最初の TODOリスト を完了させました。

次の TODOリスト を追加する前にテストコードに メソッドの抽出 を適用して リファクタリング を実施しました。リファクタリング を実施してテストが壊れていないことを確認してから TODOリスト を追加して次の作業に入りました。次の作業ではまず TODOリスト を追加してその内容を 明白な実装 で片付けました。

明白な実装 により再びテストがレッドからグリーンになったので クラスの抽出setメソッドの削除 を適用してリファクタリング を実施することにより 値オブジェクト を追加しました。
リファクタリング を実施してテストが壊れていないことを確認したら 次は例外ケースの TODOリスト を追加しました。

追加した例外ケースを 明白な実装 で片付けたら、まずテストコードに メソッドのインライン化 を適用して プロダクトコードに キーワード引数の導入 を適用してコードの可読性を改善する リファクタリング を実施しました。

仕上げに モジュール分割 を実施しました。
続いて 静的コードコードカバレッジ を実施してコードの品質を確認して、最初のリリースを完了しました。

今回のテーマである 値オブジェクト は書籍『テスト駆動開発』では 第1部 他国通貨 の中でMoneyクラスとして実装されていますし Value Objectパターン として紹介されています。

Value Objectパターン

広く共有されるものの、同一インスタンスであることはさほど重要でないオブジェクトを設計するにはどうしたらよいだろうか-----オブジェクト作成時に状態を設定したら、その後決して変えないようにする。オブジェクトへの操作は必ず新しいオブジェクトを返すようにしよう。

— テスト駆動開発

また、書籍『リファクタリング』では 第3章 コードの不吉な臭い の中の 基本データ型への執着 で言及されています。

基本データ型への執着

オブジェクト指向を始めたばかりの人は、小さなオブジェクトを使ってちょっとしたことをさせるのを嫌がる傾向があります。金額と通貨単位を組み合わせたMoney(貨幣)クラス、上限と下限と持つRange(範囲)クラス、電話番号や郵便番号を表すための特殊な文字列クラスなどがこの例に該当します。

— 新装版 リファクタリング

アプリケーション開発の過程でどのように 値オブジェクト を適用するかはこちらの記事をご参照ください。

今回のリリースでユーザーは登録することは出来ましたがユーザー名を変更することが出来ません。

次回は エンティティ の実装に取り組んでみたいと思います。

参考サイト

参考図書

  • ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本 (日本語) 単行本(ソフトカバー) 成瀬 允宣 (著)
    翔泳社 (2020/2/13)

  • テスト駆動開発 Kent Beck (著), 和田 卓人 (翻訳): オーム社; 新訳版 (2017/10/14)

  • 新装版 リファクタリング―既存のコードを安全に改善する― (OBJECT TECHNOLOGY SERIES) Martin
    Fowler (著), 児玉 公信 (翻訳), 友野 晶夫 (翻訳), 平澤 章 (翻訳), その他: オーム社; 新装版
    (2014/7/26)

  • リファクタリング(第2版): 既存のコードを安全に改善する (OBJECT TECHNOLOGY SERIES) Martin
    Fowler (著), 児玉 公信 (翻訳), 友野 晶夫 (翻訳), 平澤 章 (翻訳), その他: オーム社; 第2版
    (2019/12/1)

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした