これは【その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. お題:二郎呪文生成
下記の入力値から、二郎の呪文を生成する
ニンニク:
0: 追加無し, 1: あり, 2: 多め, 3: 非常に多め
野菜:
0: 追加無し, 1: あり, 2: 多め, 3: 非常に多め
あぶら
0: 追加無し, 1: あり, 2: 多め, 3: 非常に多め
味の濃さ
0: 普通, 1: 濃いめ
2. テストに失敗する事の確認(ニンニク呪文メソッドのredを確認)
まずは、ニンニク呪文メソッドのテスト
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
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を確認
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を確認)
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
....
Finished in 0.01458 seconds (files took 0.95896 seconds to load)
4 examples, 0 failures
4. リファクタリング
ニンニクの多さの部分は、他のトッピングでも使われるものなので、statusメソッドを作成してDRYにする。
テストコード
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
実装コード
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
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
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. 完成
ニンニク呪文以外も一通り書くと、こんな感じになりました。
テストコード
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
実装コード
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はいきなり業務で使うのは難しいので、今回のようなちょっとした実装から練習してみると良さそう
明日
【その2】ドリコム Adevent Calendar 2015 24日目はcctiger36さんです。