Endo Tech Blog

Techブログと言う名のただのブログです。

テスト駆動開発入門

この記事はGMOペパボ Advent Calendar 2017 の19日目の記事です。

昨日は@Takuma Kumeさんの記事でeBPF入門についてKixs vol.006で登壇したでした!

今日は絶賛新卒研修中で、来年からの配属に武者震いが止まらない@Fendo181が担当します。

はじめに

新卒研修も終盤に迫っていて、OJTという形で色々な部署に回っているのですが、自分はテストコードを書く機会が増えたのでテストの話をしたいなと思います。

きっかけは、テストコードについてけんちゃんくんさんに相談した際に、「今すぐ付録Cを読むんだ!」っともの凄い勢いで言われ、テスト駆動開発の付録Cを読み進めた所、テストコードの在り方だけではなく、プログラマーとしてコードをどう書くべきなのか?についても熱く語られている内容で、個人的にめちゃめちゃ感銘を受けました。

なのでテスト駆動開発について語ろうと思うのと、この本に付録Bでついている「フィボナッチ数列」をTDDで実装している例が量的に僕みたいなテストコードの書き慣れてない人でも、十分に雰囲気をつかめる内容だったのでこちらを参照しながら、テスト駆動開発を行いたいと思います。

テスト駆動開発とは何か?

この本を読んで素晴らしいと思った箇所はテスト駆動開発について最初に以下のように説明されている部分です。

テスト駆動開発はプログラミング中の不安をコントロール手法だ。ここでは「不安」は悪い意味で使っているのではない(我々は赤ちゃんではないからね)。「これは困難な問題なので、最初から全てを見通せるわけではない」という真っ当な感覚の事だ。もしも痛みが身体からの「止まれ」というサインならば不安は「気をつけろ」というサインだ。慎重になるのは良い事だが、不安には悪い効果もある。

これを読んだ時に、自分は半年前の社内の開発合宿でwebサービスをテストコード無しで開発していたのを思い出しました。 その時は時間の制限があったのでテストは一切かかずに開発をしていたのですが、気持ちは懐中電灯を持たずに真っ黒なトンネルを歩くような感じです。 不安だらけで、とにかく書いたコードが動く事を前提にしていたので「落ちる」っという事は想定してません。

そうなるとどうなるか? 答えは簡単で不安じゃない方法に逃げます。 つまりは、バグが生みそうな、あやふやなコードを量産して、一時的な安泰が欲しくて大量のコピー&ペーストを繰り返します。 なので、結果こういう事が起こる事もあって、ちゃんとテストは書いた方が良いことに越した事はないです。本当に...

このテスト駆動開発(Test-Driven-Development: TDD)には以下の2つのルールがあります。

  • 自動化されたテストが失敗した時のみ、新しいコードを書く。
  • 重複を消去する。

このルールに従って開発を進めるのがテスト駆動開発ですが、もう一つ本書ではTDDを取り入れるリズムを以下のように説明しています。

  • 1.まずはテストを1つ書く。
  • 2.すべてのテストを走らせ、新しいテストの失敗を確認する。
    • RED:動作しない、おそらく最初のうちはコンパイルも通らないテストを1つ書く。
  • 3.小さな変更を行う
  • 4.すべてのテストを走らせ、すべてを成功することを確認する。
    • GREEN:そのテストを迅速に動作させる。このステップでは罪を犯しても良い。
  • 5.リファクタリングを行って重複を消去する。
    • リファクタリング:テストを通す為に発生した重複を全て消去する。(詳細は本書の第31章リファクタリングを参照)

TDDはこのリズムの繰り返しですが、これこそがテスト駆動開発の醍醐味だと自分は感じます。 つまり、最初に不安要素にメスを入れながら、「オブジェクの状態はどうあるべきか?」、「メソッドを実行したら何が起きて欲しいのか?」を設計しながら、テストコードを書いていき徐々に改善して実装する流れになります。

ここで誤解して欲しくないのはTDDはテストファーストだから、最初に沢山テストを書く事が大事だ!っと思われがちですが、テスト駆動開発を読むとその認識は誤りだと気付かされます。 どちらかと言うと、「機能をより安全にかつ正確に設計する手法に近い」っと言うのが、今の認識です。

では今から具体的に付録Bの「フィボナッチ」を例にして、TDDで実装していきます。

フィボナッチ数列テスト駆動開発で書いてみる。

内容は本書の付録Bに記載されているフィボナッチ(Fibonacci)内容をRubyRSpecを使って書き直してみた内容です。 Githubソースコードが既に上がっています。

フィボナッチ数列をTDDを実践しながら実装する。 by Fendo181 · Pull Request #2 · Fendo181/ruby_tdd · GitHub

この付録Bは以下の本文から始まります。

本章レビュワーから質問に答える為に、フィボナッチ数列テスト駆動開発で書いた事がある。レビュワー何人かは、フィボナッチの例のおかげでTDDがどうやって機能するかを理解できたとコメントをくれた。ただ、この本は十分な長さが無いし、TDDのテクニックについても十分に説明できないので、本書の例題に置き換えるには至らなかった。もし読者の皆さんが本書の例題の部を読み終えてもまだTDDにピンきていないなら、この章から読み進めて欲しい。

付録Bは全部で4Pと短いですが、TDDの内容をつかむには十分な内容なのでまさに入門向けの題材としてここで紹介します。

1.まずはテストを1つ書く。

最初のテストはfibonacci_calc(0) =0から始まります。

require "spec_helper"

def fibonacci_calc(n)
end

describe 'Fibonacci' do
  it 'nが0の時' do
    expect(fibonacci_calc(0)).to eq (0)
  end
end

ではこれを実行してみましょう!

2.すべてのテストを走らせ、新しいテストの失敗を確認する。

F

Failures:

  1) Fibonacci nが0の時
     Failure/Error: expect(fibonacci_calc(0)).to eq (0)

       expected: 0
            got: nil

       (compared using ==)
     # ./spec/file/fibonacci_spec2.rb:8:in `block (2 levels) in <top (required)>'

Finished in 0.03248 seconds (files took 0.62911 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/file/fibonacci_spec2.rb:7 # Fibonacci nが0の時

実行結果は当然落ちます。綺麗に落ちます。 この赤い警告はこれから、何度も見るので今のうちに見慣れときましょう。本当に...嫌なぐらい見ます...。しかし、本書の力強いKent Beck氏の言葉...を翻訳した@t_wadaさんの言葉を借りれば「曖昧な状態よりもはるかに前進している」との事なので、気分を切り替えて次にいきましょう!

失敗は前進なのです!

3.小さな変更を行う

落ちたものはしょうがないので、速やかにテストを通す為に変更を行います。先程なにも処理を書いてなかったfibonacci_calcにテスト通す為に変更を加えます。

def fibonacci_calc(n)
  return 0
end 

describe 'Fibonacci' do
  it 'nが0の時' do
    expect(fibonacci_calc(0)).to eq (0)
  end
end

これはまさにn=0の時のフィボナッチ数列の挙動を表していそうに見えます。

では、試しにテストを実行してみましょう。

4.すべてのテストを走らせ、すべてを成功することを確認する。

Finished in 0.00866 seconds (files took 0.60461 seconds to load)
1 example, 0 failures

やりました!素晴らしい! この一歩はまだ小さいですが、偉大な一歩に近づいています!

ここまで来たら「もうn=1の時も簡単だ!」先に実装を頭が考えてるはずです。そうなる前に一度深呼吸して、どうあって欲しいのか?を考えながら先にテストを書きます。

describe 'Fibonacci' do
  it 'nが0の時' do
    expect(fibonacci_calc(0)).to eq (0)
  end

  it 'nが1の時' do
    expect(fibonacci_calc(1)).to eq (1)
  end
end

実行結果は当然落ちます。

.F

Failures:

  1) Fibonacci nが1の時
     Failure/Error: expect(fibonacci_calc(1)).to eq (1)

       expected: 1
            got: 0

       (compared using ==)
     # ./spec/file/fibonacci_spec2.rb:13:in `block (2 levels) in <top (required)>'

Finished in 0.04352 seconds (files took 0.44599 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/file/fibonacci_spec2.rb:12 # Fibonacci nが1の時

ですが、TDDのリズムを既に知っているので、次に何をすべきか知っています。 テストを通す為に小さく変更を行います。

def fibonacci_calc(n)
  if (n==0)
    return 0
  else
    return 1
  end
end

これでまたテストを実行します。

Finished in 0.00723 seconds (files took 0.55929 seconds to load)
2 examples, 0 failures

テストは通りました!完璧です! こんな具合にTDDでは常に細かいステップを踏む続けられる事を意識してコードを記述していきます。

5.リファクタリングを行って重複を消去する

少し先の事を考えてみましょう。

フィボナッチ数列は最初の二項が1で、第三項以降の項がすべて直前の二項の和になっている数列です。 つまりはペアが既に決まっていため、その文だけテストも多くなる事がすぐに考えつきます。

具体的にはこんな感じに...

describe 'Fibonacci' do
  it 'nが0の時' do
    expect(fibonacci_calc(0)).to eq (0)
  end

  it 'nが1の時' do
    expect(fibonacci_calc(1)).to eq (1)
  end

  it 'nが2の時' do
    expect(fibonacci_calc(2)).to eq (1)
  end

  it 'nが3の時' do
    expect(fibonacci_calc(3)).to eq (2)
  end

  it 'nが4の時' do
    expect(fibonacci_calc(5)).to eq (3)
  end
   .
   .
   .
end

テスト駆動開発の2つのシンプルなルールをもう一度思い出してみましう。

  • 自動化されたテストが失敗した時のみ、新しいコードを書く。
  • 重複を消去する。

重複をリファクタリングして消去してしまいましょう。

require "spec_helper"

def fibonacci_calc(n)
  if (n==0)
    return 0
  else
    return 1
  end
end

describe 'Fibonacci' do
  fibonacci_numbers = [[0,0],[1,1],[2,1]]

  fibonacci_numbers.size.times do |i|
    it '.fibonacci_calc' do
      expect(fibonacci_numbers[i][1]).to eq (fibonacci_calc(fibonacci_numbers[i][0]))
    end
  end
end

新しくfibonacci_numbersを導入して、フィボナッチ数列の組み合わせを二次元配列で用意しました。 これで地獄のようにテストコードをコピー&ペーストする方法から卒業できそうです。

フィボナッチ数列の実装を考える。

ここまでTDDの一連の動作を1つ1つステップで動作検証しましたが、肝心のフィボナッチ数列の実装には至ってないです。 ですが、TDDは最初に話をした通りにテスト駆動開発はプログラミング中の不安をコントロールする手法です。

これから不安なのはn=3時です。 その事を考えて先にどうあってほしいかを考えます。 答えは2が返ってきて欲しいです。 前のテストコードで記述すると以下のようになります。

it 'nが3の時' do
expect(fibonacci_calc(3)).to eq (2)
end

今の実装のままでは確実に落ちます。なので先程と同じ戦略でテストを通すコードを書いてみます。

def fibonacci_calc(n)
  if (n==0)
    return 0
  elsif (n <= 2)
    return 1
  else
    return 2
  end
end

ここまでくれば2と書いた数字は、n=1の時のペアであるから1+1を意味していたと気づきます。

def fibonacci_calc(n)
  if (n==0)
    return 0
  elsif (n <= 2)
    return 1
  else
    return 1+ 1
  end
end

ここでやっとフィボナッチ数列の中身に踏み込んで実装します。 前のほうの1はfibonacci_calc(n-1)、そして2番目の方の1はfibonacci_calc(n-2)を意味しています。

def fibonacci_calc(n)
  if (n==0)
    return 0
  elsif (n <= 2)
    return 1
  else
    return fibonacci_calc(n-1) + fibonacci_calc(n-2)
  end
end

これでn=3以降を試してもテストが通るかを確認します。

require "spec_helper"

def fibonacci_calc(n)
  if (n==0)
    return 0
  elsif (n <= 2)
    return 1
  else
    return fibonacci_calc(n-1) + fibonacci_calc(n-2)
  end
end

describe 'Fibonacci' do
  fibonacci_numbers = [[0,0],[1,1],[2,1],[3,2]]

  fibonacci_numbers.size.times do |i|
    it '.fibonacci_calc' do
      expect(fibonacci_numbers[i][1]).to eq (fibonacci_calc(fibonacci_numbers[i][0]))
    end
  end
end

テスト結果

Finished in 0.01061 seconds (files took 0.81413 seconds to load)
4 examples, 0 failures

おめでとうございます! そしてお疲れ様でした! これでテスト駆動で開発されたフィボナッチが完成しました。

余談

本書の内容や付録Cが素晴らしいのは十分に承知の話ですが、それに加えてこの本が素晴らしいと思うのはペアプロをしてるように頭の奥で語りかけられながら進めれる点だと思います。 つまりKent Beck氏と一緒にTDDを進められる感覚を味わえる...というと大げさかもしれないですが、手を動かして本を読み進めるそういう感覚になる時があります。

またどうしてもこの本を読んで思わずにいられないのは、この本は確かにテスト駆動開発という名のテスト開発に特化した技術書でもあるのですが同時にKent Beck氏のプログラマ人生を語った書籍でもあるなと感じました。 でなければ本書の中でいきなり「椅子だけはとにかく良いものを使おう。背中が痛ければ良いコードは書けない」とユーモア溢れる台詞なんて登場してきませんから。なので個人的に第26章の「休憩」の章も読み応えがある素晴らしい内容だと思いました。

そんな訳で、テストコードに対して不安要素を持ってる方は「テスト駆動開発」を一読して見て下さい。 そして皆さんが仰っている通り内容は素晴らしく、付録Cだけでも十分に元はとれます。

おわり

https://www.amazon.co.jp/テスト駆動開発-Kent-Beck/dp/4274217884

テスト駆動開発

テスト駆動開発