二郎を思い浮かべながら、RSpecを学んだことの振り返り

  • 16
    Like
  • 0
    Comment
More than 1 year has passed since last update.

これは【その2】ドリコム Advent Calendar 2015の23日目の記事です。
22日目の記事はyamteさんのバトルの体感を可視化したいです。

【その1】ドリコム Advent Calendar 2015もあわせてどうぞ。

自己紹介

新卒でドリコムに入社して、現在3年目です。
ドリコムでは、サーバサイドエンジニアをさせて頂いています。

ちなみにですが、好きな食べ物は、焼き肉、寿司、二郎です。
僕が普段、目黒の二郎で使う呪文は、「野菜ニンニク辛め」です。
職場から目黒二郎まで近いので、たまに平日にも行くのですが、平日昼間にニンニク入れてしまうと、僕の席の近くの人に申し訳ないので、最近は「野菜辛め」で我慢しています。

今日のネタ

今日は、テストコードを書くようになってからの学びについて書きたいと思います。
(急いでてもちゃんとテストを書こう!という自戒も含めてw)

テストを書くことについて

テストを書く書かない事に関しては、チーム毎に色んな考えがあると思いますが、3年間の中で学んだ事としては、テストは中途半端にやるのが一番良くないという事です。

チームの方針が、「余裕があればテストを書く」ではどんどん後回しになってしまい、テストコードの運用が大変な事になってしまいます。
後回しが続くと、結局、1からテストを書き直した方が早いという事にもなったりするので、テストコードを書くと決めた以上は、チーム全員が必ずテストを書くようにする必要があります。

テストを書くことで、綺麗な設計

テストコードを書いてて実感した事が、テストコードを書けば、自然と綺麗な設計になっていくということでした。

テストコードを書くと、テストコードを書きやすいように、実装コードを書いていくようになります。
なので、自然と他人が見ても見易いコードになっていきます。
例えば、一つのメソッドに色んな処理が入って複雑になっているとテストが書きにくいので、テストが書きやすいように、メソッドの分割等をするようになります。

RSpecの書き方

RSpecによるユニットテストの書き方
https://recompile.net/posts/how-to-write-unit-test-with-rspec.html
改めて学ぶ RSpec
http://magazine.rubyist.net/?0035-RSpecInPractice

RSpecの基本的な所は、先輩から教えて頂いた上記のページから学びました。

TDD(テスト駆動開発)

TDDとは

TDDとは、まずテストコードから書き始めて、テストに通るように実装を行う開発スタイルのことです。

テスト駆動開発/振る舞い駆動開発を始めるための基礎知識
http://www.atmarkit.co.jp/ait/articles/1403/05/news035.html
TDDの詳細は、上記のページが参考になります。

TDD勉強会

社内で以前、TDDを学ぶ機会として、週1でTDD勉強会が開催されていました。
その勉強会は、麻雀をお題にしてTDDを実践してみる勉強会で、毎回ルールを1つ追加して、そのルールに対してTDDで実装するというものでした。

TDD勉強会の良い所は、TDDを練習出来るだけではなくて、他の人の書いたコードも見れることで、テストコードの書き方も学ぶ事が出来ます。
あと、いきなり業務でTDDを実践するのは厳しいと思うので、こういうちょっとした実装からTDDを練習した方がTDDに入りやすいなと思いました。

社内のTDD勉強会に関しては、下記のページで詳しく書かれてあるので、気になられた方は見て頂ければと思います。

週刊TDD(社内TDD勉強会)紹介
http://www.slideshare.net/sue445/weekly-tdd

TDDサンプル

TDD勉強会を思い出したら、ちょっとTDDやってみたくなって、コード書いちゃいました!w

下記の手順で進めていきます。
1. お題決め
2. テストに失敗することの確認
3. テストが通ることの確認
4. 実装コードのリファクタリング
5. テストコードのリファクタリング
6. 完成

1. お題:二郎呪文生成

下記の入力値から、二郎の呪文を生成する

ニンニク: 
  0: 追加無し, 1: あり, 2: 多め, 3: 非常に多め
野菜:
  0: 追加無し, 1: あり, 2: 多め, 3: 非常に多め
あぶら
  0: 追加無し, 1: あり, 2: 多め, 3: 非常に多め
味の濃さ
  0: 普通,    1: 濃いめ

2. テストに失敗する事の確認(ニンニク呪文メソッドのredを確認)

まずは、ニンニク呪文メソッドのテスト

jiro_spec.rb
describe "Jiro" do
  options = {ninniku: 1, yasai: 1, abura: 1, karame: 1}
  let(:jiro) { Jiro.new(options) }
  describe "#ninniku_jumon" do
    subject { jiro.ninniku_jumon }
    context "ニンニク無し" do
      before do
        allow(jiro).to receive(:ninniku).and_return(0)
      end
      it { should eq "" }
    end

    context "ニンニクあり" do
      before do
        allow(jiro).to receive(:ninniku).and_return(1)
      end
      it { should eq "ニンニク" }
    end

    context "ニンニク増し" do
      before do
        allow(jiro).to receive(:ninniku).and_return(2)
      end
      it { should eq "ニンニク増し" }
    end

    context "ニンニク増し増し" do
      before do
        allow(jiro).to receive(:ninniku).and_return(3)
      end
      it { should eq "ニンニク増し増し" }
    end
  end
end
jiro.rb
class Jiro
  def initialize(options)
    @ninniku = options[:niniku]
    @yasai   = options[:yasai]
    @abura   = options[:abura]
    @karame  = options[:karame]
  end
  attr_accessor :ninniku, :yasai, :abura, :karame

  def ninniku_jumon
    nil
  end
end

ニンニク呪文メソッドのredを確認

ninniku_jumon_spec_result.rb
Failures:

  1) Jiro #ninniku_jumon ニンニク無し should eq ""
     Failure/Error: it { should eq "" }

       expected: ""
            got: nil

       (compared using ==)
     # ./spec/lib/jiro_spec.rb:11:in `block (4 levels) in <top (required)>'

  2) Jiro #ninniku_jumon ニンニクあり should eq "ニンニク"
     Failure/Error: it { should eq "ニンニク" }

       expected: "ニンニク"
            got: nil

       (compared using ==)
     # ./spec/lib/jiro_spec.rb:16:in `block (4 levels) in <top (required)>'

  3) Jiro #ninniku_jumon ニンニク増し should eq "ニンニク増し"
     Failure/Error: it { should eq "ニンニク増し" }

       expected: "ニンニク増し"
            got: nil

       (compared using ==)
     # ./spec/lib/jiro_spec.rb:21:in `block (4 levels) in <top (required)>'

  4) Jiro #ninniku_jumon ニンニク増し増し should eq "ニンニク増し増し"
     Failure/Error: it { should eq "ニンニク増し増し" }

       expected: "ニンニク増し増し"
            got: nil

       (compared using ==)
     # ./spec/lib/jiro_spec.rb:26:in `block (4 levels) in <top (required)>'

Finished in 0.00779 seconds (files took 0.52732 seconds to load)
4 examples, 4 failures

3. テストが通るように実装(ニンニク呪文メソッドのgreenを確認)

jiro.rb
class Jiro
  def initialize(options)
    @ninniku = options[:niniku]
    @yasai   = options[:yasai]
    @abura   = options[:abura]
    @karame  = options[:karame]
  end
  attr_accessor :ninniku, :yasai, :abura, :karame

  def ninniku_jumon
    if ninniku > 0
      "ニンニク" + "増し" * (ninniku-1)
    else
      ""
    end
  end
end
success_rspec.rb
....

Finished in 0.01458 seconds (files took 0.95896 seconds to load)
4 examples, 0 failures

4. リファクタリング

ニンニクの多さの部分は、他のトッピングでも使われるものなので、statusメソッドを作成してDRYにする。

テストコード

jiro_spec.rb
describe "Jiro" do
  options = {ninniku: 1, yasai: 1, abura: 1, karame: 1}
  let(:jiro) { Jiro.new(options) }

  describe "#ninniku_jumon" do
    # 省略 
  end

  describe "#status" do
    subject { jiro.status(value) }
    context "input value: 0" do
      let(:value) { 0 }
      it { should eq "" }
    end
    context "input value: 1" do
      let(:value) { 1 }
      it { should eq "" }
    end
    context "input value: 2" do
      let(:value) { 2 }
      it { should eq "増し" }
    end
    context "input value: 3" do
      let(:value) { 3 }
      it { should eq "増し増し" }
    end
  end

実装コード

jiro.rb
class Jiro
  def initialize(options)
    @ninniku = options[:niniku]
    @yasai   = options[:yasai]
    @abura   = options[:abura]
    @karame  = options[:karame]
  end
  attr_accessor :ninniku, :yasai, :abura, :karame

  def ninniku_jumon
    ninniku > 0 ? "ニンニク" + status(ninniku) : ""
  end

  def status(value)
    "増し" * (value-1)
  end
end

5. テストコードのリファクタリング

rspec-parameterized
https://github.com/tomykaira/rspec-parameterized

今回のように、条件分岐がある場合は、上記のgemを入れる事で下記のように書く事ができます。

before

jiro_spec_before.rb
  describe "#ninniku_jumon" do
    options = {ninniku: 1, yasai: 1, abura: 1, karame: 1}
    let(:jiro) { Jiro.new(options) }
    subject { jiro.ninniku_jumon }
    context "ニンニク無し" do
      before do
        allow(jiro).to receive(:ninniku).and_return(0)
      end
      it { should eq "" }
    end

    context "ニンニクあり" do
      before do
        allow(jiro).to receive(:ninniku).and_return(1)
      end
      it { should eq "ニンニク" }
    end

    context "ニンニク増し" do
      before do
        allow(jiro).to receive(:ninniku).and_return(2)
      end
      it { should eq "ニンニク増し" }
    end

    context "ニンニク増し増し" do
      before do
        allow(jiro).to receive(:ninniku).and_return(3)
      end
      it { should eq "ニンニク増し増し" }
    end

after

jiro_spec.rb
  describe "#ninniku_jumon" do
    options = {ninniku: 1, yasai: 1, abura: 1, karame: 1}
    let(:jiro) { Jiro.new(options) }
    subject { jiro.ninniku_jumon }

    using RSpec::Parameterized::TableSyntax
    where(:status, :ninniku_jumon_answer) do
      0 | ""
      1 | "ニンニク"
      2 | "ニンニク増し"
      3 | "ニンニク増し増し"
    end

    with_them do
      before do
        allow(jiro).to receive(:ninniku).and_return(status)
      end
      it { should eq ninniku_jumon_answer }
    end    
  end

6. 完成

ニンニク呪文以外も一通り書くと、こんな感じになりました。

テストコード
jiro_spec.rb

describe "Jiro" do
  options = {ninniku: 1, yasai: 1, abura: 1, karame: 1}
  let(:jiro) { Jiro.new(options) }
  using RSpec::Parameterized::TableSyntax

  describe "#ninniku_jumon" do
    subject { jiro.ninniku_jumon }
    where(:status, :ninniku_jumon_answer) do
      0 | ""
      1 | "ニンニク"
      2 | "ニンニク増し"
      3 | "ニンニク増し増し"
    end

    with_them do
      before do
        allow(jiro).to receive(:ninniku).and_return(status)
      end
      it { should eq ninniku_jumon_answer }
    end    
  end

  describe "#yasai" do
    subject { jiro.yasai_jumon }
    where(:status, :yasai_jumon_answer) do
      0 | ""
      1 | "野菜"
      2 | "野菜増し"
      3 | "野菜増し増し"
    end

    with_them do
      before do
        allow(jiro).to receive(:yasai).and_return(status)
      end
      it { should eq yasai_jumon_answer }
    end    
  end

  describe "#abura" do
    subject { jiro.abura_jumon }
    where(:status, :abura_jumon_answer) do
      0 | ""
      1 | "あぶら"
      2 | "あぶら増し"
      3 | "あぶら増し増し"
    end

    with_them do
      before do
        allow(jiro).to receive(:abura).and_return(status)
      end
      it { should eq abura_jumon_answer }
    end
  end

  describe "karame" do
    subject { jiro.karame_jumon }
    context "普通" do
      before do
        allow(jiro).to receive(:karame).and_return(0)
      end
      it { should eq "" }
    end

    context "濃いめ" do
      before do
        allow(jiro).to receive(:karame).and_return(1)
      end
      it { should eq "カラメ" }
    end
  end

  describe "#status" do

    where(:value, :status_answer) do
      0 | ""
      1 | ""
      2 | "増し"
      3 | "増し増し"
    end

    with_them do
      subject { jiro.status(value) }
      it { should eq status_answer }
    end
  end

  describe "#jumon" do
    subject { jiro.jumon }
    where(:ninniku, :yasai, :abura, :karame, :answer) do
      0 | 0 | 0 | 0 | "普通"
      0 | 1 | 1 | 0 | "野菜あぶら"
      1 | 2 | 0 | 1 | "ニンニク野菜増しカラメ"
      3 | 0 | 2 | 1 | "ニンニク増し増しあぶら増しカラメ"
    end

    with_them do
      before do
        allow(jiro).to receive(:ninniku).and_return(ninniku)
        allow(jiro).to receive(:yasai).and_return(yasai)
        allow(jiro).to receive(:abura).and_return(abura)
        allow(jiro).to receive(:karame).and_return(karame)
      end
      it { should eq answer }
    end   
  end
end
実装コード
jiro.rb
class Jiro
  def initialize(options)
    @ninniku = options[:ninniku]
    @yasai   = options[:yasai]
    @abura   = options[:abura]
    @karame  = options[:karame]
  end
  attr_accessor :ninniku, :yasai, :abura, :karame

  def ninniku_jumon
    p ninniku
    ninniku > 0 ? "ニンニク" + status(ninniku) : ""
  end

  def yasai_jumon
    yasai > 0 ? "" + "野菜" + status(yasai) : ""
  end

  def abura_jumon
    abura > 0 ? "" + "あぶら" + status(abura) : ""
  end

  def karame_jumon
    karame == 0 ? "" : "カラメ"
  end

  def status(value)
    return "" if value <= 1
    "増し" * (value-1)
  end

  def jumon
    answer = ninniku_jumon + yasai_jumon + abura_jumon + karame_jumon
    answer.empty? ? "普通" : answer
  end
end

まとめ

  • テストは中途半端にやるのが一番良くない
  • テストを書くことで、綺麗な設計になっていく
  • TDDはいきなり業務で使うのは難しいので、今回のようなちょっとした実装から練習してみると良さそう

12434542_10205103026925768_1826476810_n.jpg

明日

【その2】ドリコム Adevent Calendar 2015 24日目はcctiger36さんです。