[社内勉強会レポート]『テスト駆動開発』勉強会 ~実践編~ 01

2018.01.29

はじめに

こんにちは。2018年1月にクラスメソッドに入社した木田です。レッドブルとカレーが大好きです。

クラスメソッド社内では『テスト駆動開発』の勉強会をしており、初めてそのTDD勉強会に参加させてもらいました。

今回のレポートは、その勉強会で行った内容になります。

開発環境

  • Ruby 2.4.2
  • Rspec 3.7.0

課題について

今回はポーカーに関する課題を行いました。 課題の説明については、TDDBC仙台07課題:ポーカーから引用させていただきました。

まず、ポーカーとはなにか、使用するカードについて見ていきましょう。

ポーカー

ポーカー(poker)は、トランプを使って行うゲームのジャンルである。 プレイヤー達は5枚の札でハンド(役、手役)を作って役の強さを競う。

トランプ

トランプは、日本ではカードを使用した室内用の玩具を指すために用いられている用語で、もっぱら4種各13枚の計52枚(+α)を1セットとするタイプのものを指して言うことが多い。

  • カード (card)
    • スートとランクを持つ
  • スート (suit) - 以下の4種類を持つ
    • ♠ (スペード/spade)
    • ♥ (ハート/heart)
    • ♣ (クラブ/club)
    • ♦︎ (ダイヤ/diamond)
  • ランク (rank) - 以下の13種類を持つ
    • A (エース/ace), 2, 3, 4, 5, 6, 7, 8, 9, 10, J(ジャック/jack), Q(クイーン/queen), K(キング/king)
  • カードひと組(4スート x 13ランク = 52枚)のことをデッキ(deck)と呼ぶ

課題1 トランプのカード

課題1-1 カードの文字列表記

任意のカード1枚について、その文字列表記を取得してください

  • スート (suit) と ランク (rank) を与えて カード (card) を生成してください
  • 生成したカードから文字列表記 (notation) を取得してください

課題1-2 カードの比較

任意のカード2枚について、同じスート/ランクを持つか判断してください

  • カード (card) がもう1枚のカードと同じスートを持つか (has same suit) を判断してください
  • カード (card) がもう1枚のカードと同じランクを持つか (has same rank) を判断してください

勉強会でやったこと

課題の整理

まず、いきなり実装に入るのではなく何を実装する必要があるのか、TODOリストを作成して課題を整理することから始めました。

そうすることで、何をする必要があるのか明確になるので、迷わずコードを書くことができます。

TODOリストはコメントに書いておくと、画面の移動なくTODOを読むことができるので良いと思います。

課題の実装

プログラミング言語はRubyでテストフレームワークはRSpecを利用しました。

また、勉強会の参加者は4人いたので、2グループに分かれてグループごとにドライバーとナビゲーターに分かれて、ペアプロ形式で実装を行いました。

発表

課題終了後はグループごとに、どのようにテストコード等を書いたかを発表しました。

  • 各グループでRubyとSwiftで実装していたため、言語間のテストコードの書き方の差異があり楽しい
  • テストコードをこう書くと尚良いみたいなアドバイスをもらえるのは純粋に勉強になる
  • 他の人がテストコードを書くときにどういうことを考えて書いているか、道筋を知ることができるのは学びになる

テスト駆動開発の流れ

課題に取り組む際、どのようなテストコードを書いていったかを説明します。

テスト駆動開発では、RED、GREEN、REFACTORINGという3つの作業を繰り返します。

  • RED ... 失敗するテストを書く
  • GREEN ... テストを成功させるコードを書く
  • REFACTORING ... リファクタリングを行う

 

では、課題1-1を参考に進めてみたいと思います。

課題1-1には、「suitとrankを与えてCardを生成する」とありましたが、もっと小さく始めてみたいと思います。

そもそもCardのインスタンスを取得できるかどうかからテストを始めてみます。

describe Card do
  context '課題1-1' do
    it 'Cardを生成できること' do
      card = Card.new
      expect(card.instance_of?(Card)).to be_truthy
    end
  end
end

この状態でテストを実行すると、Cardクラスが未定義でNameError: uninitialized constant Cardと怒られます。

なので、Cardクラスを定義してあげましょう。

class Card
end

describe Card do
  context '課題1-1' do
    it 'Cardを生成できること' do
      card = Card.new
      expect(card.instance_of?(Card)).to be_truthy
    end
  end
end

これで、Cardを生成できることのテストが通りました。

次はもともとの課題である、suitとrankを与えてCardを生成できることのテストコードを書きましょう。

Cardクラスの初期化の際に、引数にsuitとrankを与えてあげます。

class Card
end

describe Card do
  context '課題1-1' do
    it 'suitとrankを与えてCardを生成できること' do
      card = Card.new('♠', '3')
      expect(card.instance_of?(Card)).to be_truthy
    end
  end
end

これでテストを実行すると、引数の数が合っておらず、ArgumentError: wrong number of arguments (given 2, expected 0)のようなエラーが発生します。

次はテストをGREENにするために、Cardクラスの初期化の際に、suitとrankを受け取れるように修正します。

class Card
  def initialize(suit, rank)
    @suit = suit
    @rank = rank
  end
end

describe Card do
  context '課題1-1' do
    it 'suitとrankを与えてCardを生成できること' do
      card = Card.new('♠', '3')
      expect(card.instance_of?(Card)).to be_truthy
    end
  end
end

これで、suitとrankを与えてCardを生成できることのテストを通すことができました。

課題1-1には、もう一つやることがありましたね。それは、「生成したインスタンスから文字列表記(notation)を取得する」というものでした。

これもまた先程と同じようにテストコードを書いていきます。

 

class Card
  def initialize(suit, rank)
    @suit = suit
    @rank = rank
  end
end

describe Card do
  context '課題1-1' do
    it 'suitとrankを与えてCardを生成できること' do
      card = Card.new('♠', '3')
      expect(card.instance_of?(Card)).to be_truthy
    end

    it '生成したインスタンスから文字列表記(notation)を取得できる' do
      card = card = Card.new('♠', '3')
      expect(card.notation).to eq '3♠'
    end
  end 
end

テストを実行すると、Cardクラスにnotationメソッドを定義していないので次のように怒られます。NoMethodError: undefined method 'notation' for #<Card:0x00007ff5e23e86b0 @suit="♠", @rank="3">

次は、Cardクラスにnotationメソッドを定義してあげましょう。

class Card
  def initialize(suit, rank)
    @suit = suit
    @rank = rank
  end

  def notation
    "#@rank#@suit"
  end
end

describe Card do
  context '課題1-1' do
    it 'suitとrankを与えてCardを生成できること' do
      card = Card.new('♠', '3')
      expect(card.instance_of?(Card)).to be_truthy
    end

    it '生成したインスタンスから文字列表記(notation)を取得できる' do
      card = card = Card.new('♠', '3')
      expect(card.notation).to eq '3♠'
    end
  end
end

これで、生成したインスタンスから文字列表記(notation)を取得できるのテストが通りました。

テストコードを見てみると次の重複したコードがありますよね?card = Card.new('♠', '3')

これをリファクタリングするとこうなります。

class Card
  def initialize(suit, rank)
    @suit = suit
    @rank = rank
  end

  def notation
    "#@rank#@suit"
  end
end

describe Card do
  let(:card) { Card.new('♠', '3') }

  context '課題1-1' do
    it 'suitとrankを与えてCardを生成できること' do
      expect(card.instance_of?(Card)).to be_truthy
    end

    it '生成したインスタンスから文字列表記(notation)を取得できる' do
      expect(card.notation).to eq '3♠'
    end
  end
end

これでテストコードがスッキリしましたね!

以上のような流れで、テスト駆動開発で課題を行いました。

最後に

今回、小さな課題でしたがテスト駆動の作業サイクルである、RED、GREEN、REFACTORINGで、実際にコードを書いてみることでTDDの雰囲気を掴むことはできたかなと思います。

僕自身、まだまだTDDについて知らないことが多いので、『テスト駆動開発』を読みつつ、実践してみて、学んだことはレポートとして共有できたらと思います。

以上!