0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【TDD】名著『テスト駆動開発』をRuby + RSpecで写経する (第2章)

0
Posted at

はじめに

前準備~第1章までの内容については以下の記事にまとめています!

第2章 明白な実装

作成するコードについて

第1章では「ドルの掛け算」を実装しましたが、現在のコードには大きな設計上の問題が残っています。
それは、Dollar オブジェクトに対して times を実行すると、オブジェクト自身の値(インスタンス変数)を書き換えてしまうという点です。

これを現実の「5ドル紙幣」に例えると、2倍の計算をしたら手元の5ドル札が10ドル札に魔法のように書き換わってしまうようなもので、プログラミングにおいては予期せぬ挙動(副作用)の原因となります。

【現在のToDoリスト】

  • \$5 + 10 CHF = $10 (レートが2:1の場合)
  • \$5 * 2 = \$10
  • amountをprivateにする
  • Dollarの副作用どうする? ← 今からここに着手
  • Moneyの丸め処理どうする?

1. 失敗するテストを書く(Red)

上記のように、計算を行っても元オブジェクトが変更されないことを確かめるテストを書きます。

# frozen_string_literal: true

# spec/dollar_spec.rb
require 'dollar'

RSpec.describe 'Dollar' do
  it 'multiplication' do
    five = Dollar.new(5)

    # 1回目の掛け算:2 * 5 = 10 を期待
    five.times(2)
    expect(five.amount).to eq 10

    # 2回目の掛け算:元の five は 5 のままであり、 5 * 3 = 15 を期待
    five.times(3)
    expect(five.amount).to eq 15
  end
end

このテストを実行すると、2回目の expect で結果が 30(10 * 3)になってしまい、失敗します。

実行結果
$ bundle exec rspec

Dollar
  multiplication (FAILED - 1)

Failures:

  1) Dollar multiplication
     Failure/Error: expect(five.amount).to eq 15
     
       expected: 15
            got: 30
     
       (compared using ==)
     # ./spec/dollar_spec.rb:16:in 'block (2 levels) in <top (required)>'

Finished in 0.00769 seconds (files took 0.04801 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/dollar_spec.rb:7 # Dollar multiplication

上記のような意図のテストを通すためには、timesメソッドで新しいDollarのインスタンスが返るようになればよさそうです。
$5 * 2 の結果は \$10のDollarオブジェクトが返ればよさそうです。テストコードを以下のように修正します。

# frozen_string_literal: true

require 'dollar'

RSpec.describe 'Dollar' do
  it 'multiplication' do
    five = Dollar.new(5)

    # timesメソッドが新しいオブジェクトを返すと定義する
    product = five.times(2)
    expect(product.amount).to eq 10

    # 元の five オブジェクトは 5ドルのままであることを期待する
    product = five.times(3)
    expect(product.amount).to eq 15
  end
end

いずれにせよ、現時点ではテストは失敗します。

2. 実装でテストを通す(Green)

第1章では、とにかく最短でテストを通すために amount = 10 と記述する「仮実装」を行いました。

しかし、今回の「計算結果として新しいオブジェクトを返す」という修正は、実装方法が非常に明白です。このように、実装がすぐに思い浮かぶ場合に最初から正解を書く手法を、本書では 「明白な実装(Obvious Implementation)」 と呼んでいます。

戦略 概要
仮実装 コードにべた書きで値を書き、徐々に変数化
明白な実装 すぐに頭の中の実装をコードに落とし込む

著者であるKent Beck氏は、上記の2つのモードを揺れ動きながら実装を進めていると述べています。
書くべき内容がはっきりしているときは 明白な実装 から 明白な実装 へ、予期せずテストが失敗したら 仮実装 モードで細かく進めていく方針をとる、とのことです。

テストを通すため、Dollarクラスのtimesメソッドの内容を修正します。

# frozen_string_literal: true

class Dollar
  attr_reader :amount

  def initialize(amount)
    @amount = amount
  end

  def times(multiplier)
    Dollar.new(@amount * multiplier) # ←☆ Dollarインスタンスを生成して返す
  end
end

これでテストは通るようになり、副作用の問題を解決できました!

このように、一度作成されたら値が変わらないオブジェクトを Value Object(値オブジェクト) と呼びます。
しかし、「値オブジェクト」になったことで、今度は「5ドルのオブジェクト同士が等しいか」を判断する必要が出てきました。次回はそこに焦点を当てます。

【現在のToDoリスト】

  • \$5 + 10 CHF = $10 (レートが2:1の場合)
  • \$5 * 2 = \$10
  • amountをprivateにする
  • Dollarの副作用どうする?
  • Moneyの丸め処理どうする?
0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?