初めに この記事は書籍『ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本』 で解説されているドメイン駆動設計パターンをテスト駆動開発で実装したものです。
言語は Ruby です。Ruby でのテスト駆動開発の詳細に関してはこちらの記事 をご参照ください。Gitpod からブラウザ開発環境が利用できるのでお手軽に開発を始めることが出来ます。
ユーザーストーリー まず ユーザーストーリー をもとに仕様を整理します。
ユーザーストーリーとは、ソフトウェアシステムに求められるふるまいをまとめたものだ。アジャイルソフトウェア開発の世界で広く使われており、大量の機能を細かく分解して計画作りに生かせるようにしている。 同じような概念を表す用語としてフィーチャーという言い方もあるが、 最近のアジャイル界隈では「ストーリー」とか「ユーザーストーリー」とかいう用語のほうが広まっている。
— Martin Fowler’s Bliki (ja) https://bliki-ja.github.io/UserStory
SNS(ソーシャルネットワークサービス)のユーザー機能ということなので以下の ユーザーストーリー を作成しました。
利用者として
ユーザーを管理できるようにしたい
なぜならユーザーはシステムを利用するために必要だから
TODO リスト ユーザーストーリー を作成したらそれをもとに TODO リスト を作成します。TODO リスト はプログラムとして実行できる粒度で具体的に記述します。
仮実装 ユーザーを登録する さっそく TODO リスト の1つ目を片付けましょう。 まずは テストファースト で最初に失敗するコードを書きます。
1 2 3 4 5 6 7 8 9 class HelloTest < Minitest::Test def test_greeting assert_equal 'hello world' , greeting end end def greeting 'hello world' end
サンプルコードを以下のコードに書き換えてテストを実行します。
1 2 3 4 5 6 7 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
クラスが定義されていないからですね。
1 2 3 4 5 6 7 8 9 10 11 $ 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 クラスを追加します。 まずはテストをパスさせるために 仮実装 でベタ書きのコードを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
テストをパスさせてレッドからグリーンになりました。
1 2 3 4 5 6 7 8 $ 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
仮実装から実装へ ユーザーを登録する テストは通りましたがコードはベタ書きのままです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
仮実装 のままでは別のユーザーを作ることが出来ないので、コンストラクタ経由で作成できるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
テストが通りました。
1 2 3 4 5 6 7 8 $ 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 リスト を片付けたのでここでバージョン管理システムを使ってコミットしておきます。
1 2 $ git add . $ git commit -m 'test: ユーザーを登録する'
以下、 TODO リスト を片付けるたびにコミットしていきます。
リファクタリング
レッド・グリーンときたので続いて リファクタリング を実施します。
メソッドの抽出 本来はコードの重複が発生してからやるのですが今回は予め メソッドの抽出 を適用してフィクスチャーを抽出しておきます。
1 2 3 4 5 6 7 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 メソッドです。
1 2 3 4 5 6 7 8 9 10 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 リスト に取り掛かります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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
まず、失敗するテストを書いて 明白な実装 でテストをパスするようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 クラスから 値オブジェクト を抽出する リファクタリング を適用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 クラスを使って呼び出すように変更したらエラーを修正してグリーンの状態を維持します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 クラスを抽出します。テストコードも同様に変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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 ...
テストコードを修正してグリーンになったらコミットして クラスの抽出 の リファクタリング 完了です。
1 2 3 4 5 6 7 8 9 $ruby test /hello_test.rbStarted 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 メソッドの削除 を適用して 値オブジェクト の要求を満たす不変オブジェクトに リファクタリング しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 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
アックセッサメソッドを読み取り専用に変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ... class UserId attr_reader :value ... end class UserName attr_reader :value ... end class User attr_reader :id , :name ... end
テストが壊れていないことを確認したらコミットします。
1 2 3 4 5 6 7 8 9 $ 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つづつ片付けていくとしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 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つテストコードを追加したらプロダクトコードを実装してレッド・グリーンのサイクルを回しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 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 を指定しない場合はエラー
今回はコードの可読性を改善する観点で リファクタリング を実施してみたいと思います。
メソッドのインライン化 テストコードが増えてきましたここでテストコードをグルーピングするため メソッドのインライン化 を適用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 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 リスト の構造に合わせることで可読性を改善します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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 ...
実行結果もわかりやすいなりました。 テストは壊れていないのでコミットします。
1 2 3 4 5 6 7 8 9 10 11 12 $ 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 では型を明示しないため引数の値がリテラルなのか 値オブジェクト なのかメソッドの定義だけでは把握できません。キーワード引数の導入 をしてできるだけ引数の型を把握しやすいように リファクタリング しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 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
キーワード引数 を 値オブジェクト と同じ名称にします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 を指定しない場合はエラー
クラス図
ファイル構成 /main.rb
|--lib/
|
-- sns.rb
-- user_id.rb
-- user_name.rb
-- user.rb
|--test/
|
-- user_test.rb
/main.rb.
1 require './test/user_test.rb'
/lib/sns.rb.
1 2 3 4 5 require './lib/user_id.rb' require './lib/user_name.rb' require './lib/user.rb'
/lib/user_id.rb.
1 2 3 4 5 6 7 8 9 10 11 12 class UserId attr_reader :value def initialize (value) raise if value.nil ? @value = value end end
/lib/user_name.rb.
1 2 3 4 5 6 7 8 9 10 11 12 13 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.
1 2 3 4 5 6 7 8 9 10 11 class User attr_reader :id , :name def initialize (user_id: , user_name: ) @id = user_id @name = user_name end end
/test/user_test.rb.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 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
リリース モジュール分割 により最初のリリースの準備が出来ました。 リリース前に 静的コード解析 と コードカバレッジ を実施してコードの品質を確認しておきましょう。 手順の詳細は こちらの記事 をご参照ください。
静的コード解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 $ 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
ファイルを以下に更新します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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/**"
警告は無くなりました。
1 2 3 4 5 $ rubocop Inspecting 5 files ..... 5 files inspected, no offenses detected
コードカバレッジ まず、テストコードからコードカバレッジを実行できるようにします。
user_test.rb
の先頭を以下に更新します。
1 2 3 4 5 6 7 8 9 10 require 'simplecov' SimpleCov.start require 'minitest/reporters' Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new(color: true )] require 'minitest/autorun' require './lib/sns.rb' ...
テストを実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 $ 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%です。
ふりかえり 最初のリリースが完了したのでここでやってきたことのふりかえりをしておきましょう。
まず、ユーザーストーリー から 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)