|
| 1 | +# 第11章: テスト文化入門 - Minitest |
| 2 | + |
| 3 | +Rubyは動的型付け言語であり、コンパイル時ではなく実行時に型が決まります。これは柔軟で高速な開発を可能にする反面、型の不一致などによる単純なミスが実行時まで検出されにくいという特性も持ちます。 |
| 4 | + |
| 5 | +そのため、Rubyコミュニティでは「**テストは文化**」と言われるほど、自動化されたテストを書くことが重視されます。テストは、コードが期待通りに動作することを保証するだけでなく、未来の自分や他の開発者がコードをリファクタリング(修正・改善)する際の「安全網」として機能します。 |
| 6 | + |
| 7 | +この章では、Rubyに標準で添付されているテスティングフレームワーク「Minitest」を使い、テストの基本的な書き方と文化を学びます。 |
| 8 | + |
| 9 | +## 標準添付のテスティングフレームワーク「Minitest」 |
| 10 | + |
| 11 | +Minitestは、Rubyに標準で含まれている(=別途インストール不要)軽量かつ高速なテストフレームワークです。 |
| 12 | + |
| 13 | +Ruby on Railsなどの主要なフレームワークもデフォルトでMinitestを採用しており、Rubyのエコシステムで広く使われています。(RSpecという、よりDSL(ドメイン固有言語)ライクに記述できる人気のフレームワークもありますが、まずは標準のMinitestを理解することが基本となります。) |
| 14 | + |
| 15 | +Minitestは、`Minitest::Test` を継承する「Unitスタイル」と、`describe` ブロックを使う「Specスタイル」の2種類の書き方を提供しますが、この章では最も基本的なUnitスタイルを学びます。 |
| 16 | + |
| 17 | +## テストファイルの作成と実行 |
| 18 | + |
| 19 | +早速、簡単なクラスをテストしてみましょう。 |
| 20 | + |
| 21 | +### 1\. テスト対象のクラスの作成 |
| 22 | + |
| 23 | +まず、テスト対象となる簡単な電卓クラスを作成します。 |
| 24 | + |
| 25 | +```ruby:calculator.rb |
| 26 | +# シンプルな電卓クラス |
| 27 | +class Calculator |
| 28 | + def add(a, b) |
| 29 | + a + b |
| 30 | + end |
| 31 | + |
| 32 | + def subtract(a, b) |
| 33 | + a - b |
| 34 | + end |
| 35 | +end |
| 36 | +``` |
| 37 | + |
| 38 | +### 2\. テストファイルの作成 |
| 39 | + |
| 40 | +Rubyの規約では、テストファイルは `test_` プレフィックス(例: `test_calculator.rb`)または `_test.rb` サフィックス(例: `calculator_test.rb`)で作成するのが一般的です。ここでは `test_calculator.rb` を作成します。 |
| 41 | + |
| 42 | +テストファイルは、以下の要素で構成されます。 |
| 43 | + |
| 44 | +1. `require 'minitest/autorun'` |
| 45 | + * Minitestライブラリを読み込み、ファイル実行時にテストが自動で走るようにします。 |
| 46 | +2. `require_relative 'ファイル名'` |
| 47 | + * テスト対象のファイル(今回は `calculator.rb`)を読み込みます。 |
| 48 | +3. `class クラス名 < Minitest::Test` |
| 49 | + * テストクラスを定義し、`Minitest::Test` を継承します。 |
| 50 | +4. `def test_メソッド名` |
| 51 | + * `test_` で始まるメソッドを定義します。これが個々のテストケースとなります。 |
| 52 | + |
| 53 | +```ruby:test_calculator.rb |
| 54 | +require 'minitest/autorun' |
| 55 | +require_relative 'calculator' |
| 56 | + |
| 57 | +class CalculatorTest < Minitest::Test |
| 58 | + # `test_` で始まるメソッドがテストとして実行される |
| 59 | + def test_addition |
| 60 | + # テスト対象のインスタンスを作成 |
| 61 | + calc = Calculator.new |
| 62 | + |
| 63 | + # 期待値 (Expected) |
| 64 | + expected = 5 |
| 65 | + # 実際の結果 (Actual) |
| 66 | + actual = calc.add(2, 3) |
| 67 | + |
| 68 | + # アサーション(後述) |
| 69 | + # 期待値と実際の結果が等しいことを検証する |
| 70 | + assert_equal(expected, actual) |
| 71 | + end |
| 72 | + |
| 73 | + def test_subtraction |
| 74 | + calc = Calculator.new |
| 75 | + # アサーションは1行で書くことが多い |
| 76 | + assert_equal(1, calc.subtract(4, 3)) |
| 77 | + end |
| 78 | +end |
| 79 | +``` |
| 80 | + |
| 81 | +### 3\. テストの実行 |
| 82 | + |
| 83 | +ターミナルで、作成した**テストファイル**を実行します。 |
| 84 | + |
| 85 | +```ruby-exec:test_calculator.rb |
| 86 | +Run options: --seed 51740 |
| 87 | + |
| 88 | +# Running: |
| 89 | + |
| 90 | +.. |
| 91 | + |
| 92 | +Finished in 0.001099s, 1819.8362 runs/s, 1819.8362 assertions/s. |
| 93 | + |
| 94 | +2 runs, 2 assertions, 0 failures, 0 errors, 0 skips |
| 95 | +``` |
| 96 | + |
| 97 | +実行結果のサマリに注目してください。 |
| 98 | + |
| 99 | + * `.`(ドット): テストが成功(Pass)したことを示します。 |
| 100 | + * `2 runs, 2 assertions`: 2つのテスト(`test_addition` と `test_subtraction`)が実行され、合計2回のアサーション(`assert_equal`)が成功したことを意味します。 |
| 101 | + * `0 failures, 0 errors`: 失敗もエラーもありません。 |
| 102 | + |
| 103 | +もしテストが失敗すると、`F`(Failure)や `E`(Error)が表示され、詳細なレポートが出力されます。 |
| 104 | + |
| 105 | +## アサーション(assert\_equal, assert 等)の書き方 |
| 106 | + |
| 107 | +アサーション(Assertion = 表明、断言)は、「この値はこうあるべきだ」と検証するためのメソッドです。Minitestは `Minitest::Test` を継承したクラス内で、様々なアサーションメソッドを提供します。 |
| 108 | + |
| 109 | +### `assert_equal(expected, actual)` |
| 110 | + |
| 111 | +最もよく使うアサーションです。「期待値(expected)」と「実際の結果(actual)」が `==` で等しいことを検証します。 |
| 112 | + |
| 113 | +> **⚠️ 注意:** 引数の順序が重要です。\*\*1番目が「期待値」、2番目が「実際の結果」\*\*です。逆にすると、失敗時のメッセージが非常に分かりにくくなります。 |
| 114 | +
|
| 115 | +```ruby-repl |
| 116 | +irb> require 'minitest/assertions' |
| 117 | +=> true |
| 118 | +irb> include Minitest::Assertions |
| 119 | +=> Object |
| 120 | +irb> def assert_equal(expected, actual); super; end # irbで使うための設定 |
| 121 | +=> :assert_equal |
| 122 | +
|
| 123 | +irb> assert_equal 5, 2 + 3 |
| 124 | +=> true |
| 125 | +
|
| 126 | +irb> assert_equal 10, 2 + 3 |
| 127 | +# Minitest::Assertion: <--- 失敗(Assertion Failed) |
| 128 | +# Expected: 10 |
| 129 | +# Actual: 5 |
| 130 | +``` |
| 131 | + |
| 132 | +### `assert(test)` |
| 133 | + |
| 134 | +`test` が **true**(またはtrueと評価される値)であることを検証します。偽(`false` または `nil`)の場合は失敗します。 |
| 135 | + |
| 136 | +```ruby-repl |
| 137 | +irb> assert "hello".include?("e") |
| 138 | +=> true |
| 139 | +irb> assert [1, 2, 3].empty? |
| 140 | +# Minitest::Assertion: Expected [] to be empty?. |
| 141 | +``` |
| 142 | + |
| 143 | +### `refute(test)` |
| 144 | + |
| 145 | +`assert` の逆です。`test` が **false** または `nil` であることを検証します。 |
| 146 | + |
| 147 | +```ruby-repl |
| 148 | +irb> refute [1, 2, 3].empty? |
| 149 | +=> true |
| 150 | +irb> refute "hello".include?("e") |
| 151 | +# Minitest::Assertion: Expected "hello".include?("e") to be falsy. |
| 152 | +``` |
| 153 | + |
| 154 | +### `assert_nil(obj)` |
| 155 | + |
| 156 | +`obj` が `nil` であることを検証します。 |
| 157 | + |
| 158 | +```ruby-repl |
| 159 | +irb> a = nil |
| 160 | +=> nil |
| 161 | +irb> assert_nil a |
| 162 | +=> true |
| 163 | +``` |
| 164 | + |
| 165 | +### `assert_raises(Exception) { ... }` |
| 166 | + |
| 167 | +ブロック `{ ... }` を実行した結果、指定した例外(`Exception`)が発生することを検証します。 |
| 168 | + |
| 169 | +これは、意図したエラー処理が正しく動作するかをテストするのに非常に重要です。 |
| 170 | + |
| 171 | +```ruby:test_calculator_errors.rb |
| 172 | +require 'minitest/autorun' |
| 173 | + |
| 174 | +class Calculator |
| 175 | + def divide(a, b) |
| 176 | + raise ZeroDivisionError, "Cannot divide by zero" if b == 0 |
| 177 | + a / b |
| 178 | + end |
| 179 | +end |
| 180 | + |
| 181 | +class CalculatorErrorTest < Minitest::Test |
| 182 | + def test_division_by_zero |
| 183 | + calc = Calculator.new |
| 184 | + |
| 185 | + # ブロック内で ZeroDivisionError が発生することを期待する |
| 186 | + assert_raises(ZeroDivisionError) do |
| 187 | + calc.divide(10, 0) |
| 188 | + end |
| 189 | + end |
| 190 | +end |
| 191 | +``` |
| 192 | + |
| 193 | +```ruby-exec:test_calculator_errors.rb |
| 194 | +Run options: --seed 19800 |
| 195 | + |
| 196 | +# Running: |
| 197 | + |
| 198 | +. |
| 199 | + |
| 200 | +Finished in 0.000624s, 1602.5641 runs/s, 1602.5641 assertions/s. |
| 201 | + |
| 202 | +1 runs, 1 assertions, 0 failures, 0 errors, 0 skips |
| 203 | +``` |
| 204 | + |
| 205 | +## 簡単なTDD(テスト駆動開発)の体験 |
| 206 | + |
| 207 | +TDD (Test-Driven Development) は、機能のコードを書く前に、**まず失敗するテストコードを書く**開発手法です。TDDは以下の短いサイクルを繰り返します。 |
| 208 | + |
| 209 | +1. **Red (レッド):** |
| 210 | + * これから実装したい機能に対する「失敗するテスト」を書きます。 |
| 211 | + * まだ機能が存在しないため、テストは(当然)失敗(Red)します。 |
| 212 | +2. **Green (グリーン):** |
| 213 | + * そのテストをパスさせるための**最小限の**機能コードを実装します。 |
| 214 | + * テストが成功(Green)すればOKです。コードの綺麗さはまだ問いません。 |
| 215 | +3. **Refactor (リファクタリング):** |
| 216 | + * テストが成功した状態を維持したまま、コードの重複をなくしたり、可読性を上げたりする「リファクタリング」を行います。 |
| 217 | + |
| 218 | +`Calculator` クラスに、`multiply`(掛け算)メソッドをTDDで追加してみましょう。 |
| 219 | + |
| 220 | +### 1\. Red: 失敗するテストを書く |
| 221 | + |
| 222 | +まず、`test_calculator.rb` に `multiply` のテストを追加します。 |
| 223 | + |
| 224 | +```ruby:calculator.rb |
| 225 | +# シンプルな電卓クラス |
| 226 | +class Calculator |
| 227 | + def add(a, b) |
| 228 | + a + b |
| 229 | + end |
| 230 | + |
| 231 | + def subtract(a, b) |
| 232 | + a - b |
| 233 | + end |
| 234 | +end |
| 235 | +``` |
| 236 | + |
| 237 | +```ruby:test_calculator_tdd.rb |
| 238 | +require 'minitest/autorun' |
| 239 | +require_relative 'calculator' # calculator.rb は add と subtract のみ |
| 240 | + |
| 241 | +class CalculatorTest < Minitest::Test |
| 242 | + def setup |
| 243 | + # @calc をインスタンス変数にすると、各テストメソッドで使える |
| 244 | + @calc = Calculator.new |
| 245 | + end |
| 246 | + |
| 247 | + def test_addition |
| 248 | + assert_equal(5, @calc.add(2, 3)) |
| 249 | + end |
| 250 | + |
| 251 | + def test_subtraction |
| 252 | + assert_equal(1, @calc.subtract(4, 3)) |
| 253 | + end |
| 254 | + |
| 255 | + # --- TDDサイクル スタート --- |
| 256 | + |
| 257 | + # 1. Red: まずテストを書く |
| 258 | + def test_multiplication |
| 259 | + assert_equal(12, @calc.multiply(3, 4)) |
| 260 | + end |
| 261 | +end |
| 262 | +``` |
| 263 | + |
| 264 | +この時点で `calculator.rb` に `multiply` メソッドは存在しません。テストを実行します。 |
| 265 | + |
| 266 | +```ruby-exec:test_calculator_tdd.rb |
| 267 | +# (実行結果の抜粋) |
| 268 | +... |
| 269 | +Error: |
| 270 | +CalculatorTest#test_multiplication: |
| 271 | +NoMethodError: undefined method `multiply' for #<Calculator:0x...> |
| 272 | +... |
| 273 | +1 runs, 0 assertions, 0 failures, 1 errors, 0 skips |
| 274 | +``` |
| 275 | + |
| 276 | +期待通り、`NoMethodError` でテストが**エラー (E)** になりました。これが「Red」の状態です。(Failure (F) はアサーションが期待と違った場合、Error (E) はコード実行中に例外が発生した場合を指します) |
| 277 | + |
| 278 | +### 2\. Green: テストを通す最小限のコードを書く |
| 279 | + |
| 280 | +次に、`calculator.rb` に以下のように `multiply` メソッドを実装し、テストをパス(Green)させます。 |
| 281 | + |
| 282 | +```ruby |
| 283 | +class Calculator |
| 284 | + def add(a, b) |
| 285 | + a + b |
| 286 | + end |
| 287 | +
|
| 288 | + def subtract(a, b) |
| 289 | + a - b |
| 290 | + end |
| 291 | +
|
| 292 | + # 2. Green: テストを通す最小限の実装 |
| 293 | + def multiply(a, b) |
| 294 | + a * b |
| 295 | + end |
| 296 | +end |
| 297 | +``` |
| 298 | + |
| 299 | +`calculator.rb` を編集し、再びテストを実行すると、以下のようにすべてのテストが成功します。「Green」の状態です。 |
| 300 | + |
| 301 | +```bash |
| 302 | +$ ruby test_calculator_tdd.rb |
| 303 | +... |
| 304 | +Finished in ... |
| 305 | +3 runs, 3 assertions, 0 failures, 0 errors, 0 skips |
| 306 | +``` |
| 307 | + |
| 308 | +### 3\. Refactor: リファクタリング |
| 309 | + |
| 310 | +今回は実装が非常にシンプルなのでリファクタリングの必要はあまりありませんが、もし `multiply` の実装が複雑になったり、他のメソッドとコードが重複したりした場合は、この「Green」の(テストが成功している)状態で安心してコードをクリーンアップします。 |
| 311 | + |
| 312 | +TDDは、この「Red -\> Green -\> Refactor」のサイクルを高速で回すことにより、バグの少ない、メンテンスしやすいコードを堅実に構築していく手法です。 |
| 313 | + |
| 314 | +## 📈 この章のまとめ |
| 315 | + |
| 316 | + * Rubyは動的型付け言語であるため、実行時の動作を保証する**テストが非常に重要**です。 |
| 317 | + * **Minitest** はRubyに標準添付された軽量なテスティングフレームワークです。 |
| 318 | + * テストファイルは `require 'minitest/autorun'` し、`Minitest::Test` を継承します。 |
| 319 | + * テストメソッドは `test_` プレフィックスで定義します。 |
| 320 | + * `assert_equal(期待値, 実際の結果)` が最も基本的なアサーションです。 |
| 321 | + * `assert` (true検証), `refute` (false検証), `assert_raises` (例外検証) などもよく使われます。 |
| 322 | + * **TDD (テスト駆動開発)** は「Red (失敗) -\> Green (成功) -\> Refactor (改善)」のサイクルで開発を進める手法です。 |
| 323 | + |
| 324 | +### 練習問題1: Stringクラスのテスト |
| 325 | + |
| 326 | +`Minitest::Test` を使って、Rubyの組み込みクラスである `String` の動作をテストする `test_string.rb` を作成してください。以下の2つのテストメソッドを実装してください。 |
| 327 | + |
| 328 | + * `test_string_length`: `"hello"` の `length` が `5` であることを `assert_equal` で検証してください。 |
| 329 | + * `test_string_uppercase`: `"world"` を `upcase` した結果が `"WORLD"` であることを `assert_equal` で検証してください。 |
| 330 | + |
| 331 | +```ruby:test_string.rb |
| 332 | +require 'minitest/autorun' |
| 333 | +
|
| 334 | +
|
| 335 | +``` |
| 336 | + |
| 337 | +```ruby-exec:test_string.rb |
| 338 | +``` |
| 339 | + |
| 340 | +### 練習問題2: TDDでUserクラスを実装 |
| 341 | + |
| 342 | +TDDの「Red -\> Green」サイクルを体験してください。 |
| 343 | + |
| 344 | +1. (Red)`User` クラスに `first_name` と `last_name` を渡してインスタンス化し、`full_name` メソッドを呼ぶと `"First Last"` のように連結された文字列が返ることを期待するテスト `test_full_name` を含む `test_user.rb` を先に作成してください。(この時点では `user.rb` は空か、存在しなくても構いません) |
| 345 | +2. (Green)テストがパスするように、`user.rb` に `User` クラスを実装してください。(`initialize` で名前を受け取り、`full_name` メソッドで連結します) |
| 346 | + |
| 347 | + |
| 348 | +```ruby:user.rb |
| 349 | +``` |
| 350 | + |
| 351 | +```ruby:test_user.rb |
| 352 | +require 'minitest/autorun' |
| 353 | +require_relative 'user' |
| 354 | +
|
| 355 | +``` |
| 356 | + |
| 357 | +```ruby-exec:test_user.rb |
| 358 | +``` |
0 commit comments