概要
Convention over Configration(設定より規約。CoC) 〜 プロダクトコード・テストコード・ドキュメントの一括生成・一括更新をCoCで行ってみる
詳細
Convention over Configration(設定より規約。CoC)はRuby on Railsで利用されているパターンです。
規約を利用することで、設定ファイルなどの記述を最小限に押さえることができます。
自明のデフォルト設定については何も記述する必要がなく、
特別なケースのみ設定値を記述すれば済むようになります。
代表例
例えばRuby on RailsのActiveRecordです。
クラス名を元にテーブルとのマッピングを行います。
これにより、クラスと実テーブルのマッピングに関する設定が不要になります。
サンプル仕様
プロダクトコード、テストコード、ドキュメント
を下記の構成で作成します。(fizzbuzzクラスを作成する場合)
┣ bin ┃ ┗ generator.rb ┣ doc ┃ ┗ fizzbuzz_spec.md ┣ lib ┃ ┗ fizzbuzz.rb ┗ spec ┗ fizzbuzz_spec.rb
generator.rbはこの構成=規約を前提に各ファイルの生成や
ファイル内の一部のAPIコメントの更新を行うツールです。
generator.rbには以下の機能を持たせます。
・create機能
ruby ./bin/generator.rb create xxxx と呼び出すと
./doc/xxx.md
./lib/xxx.rb
./spec/xxx_spec.rb
を生成します。
・update_api_doc機能
ruby ./bin/generator.rb update_api_doc xxxx と呼び出すと
rspecで ./spec/xxx_spec.rb のテストを実行し、その実行結果から
xxx.mdにAPIドキュメントを生成
xxx.rbのメソッドヘッダーにも同様のAPIドキュメントを生成
します。
これによって、プロダクトコード・テストコード・ドキュメントのひな型を生成や
テストコード内のサンプルケースをもとに、プロダクトコードのAPIコメントやドキュメント内のメソッドの利用サンプルを
コマンドひとつで行うことができます。
サンプルコード
※プロダクトコードとテストコードのひな形生成には rspec_piccolo gemを利用します。
generator.rb
require 'active_support' module Generator def self.create(basename) klass = basename.capitalize `rspec --init` `piccolo e -p #{klass} #{basename} #{basename}` Dir.mkdir('doc') unless Dir.exists?('doc') File.open("./doc/#{basename}.md", 'w:utf-8') do |f| f.print <<-EOS # #{klass} class ## #{basename} method pending EOS end end def self.update_api_doc(basename) spec = "#{basename}_spec.rb" result = `rspec -fd spec/#{spec}` samples = [] result.each_line do |line| samples << line.chop.strip if line.strip.match /^Fizzbuzz.new.fizzbuzz/ end update_src_api_doc_sample(basename, samples) update_api_doc_sample(basename, samples) end private def self.update_src_api_doc_sample(basename, samples) src = File.read("./lib/#{basename}.rb") samples = samples.join("\n # ") samples = " # #{samples}\n" api_renew_src = src.gsub /(\$fizzbuzz examples start\$\n).*( # \$fizzbuzz examples end\$)/m, "\\1#{samples}\\2" File.open("./lib/#{basename}.rb", 'w:utf-8') { |f|f.print api_renew_src} end def self.update_api_doc_sample(basename, samples) klass = basename.capitalize File.open("./doc/#{basename}.md", 'w:utf-8') do |f| f.print <<-EOS # #{klass} class ## #{basename} method ~~~ruby #{samples.join("\n")} ~~~ EOS end end end command = $*[0] basename = $*[1] case command when 'create' Generator.create(basename) when 'update_api_doc' Generator.update_api_doc(basename) else fail "invalid command name #{command}" end
generator.rb createの動作検証
$ ruby bin/generator.rb create fizzbuzz $ tree ┣ bin ┃ ┗ generator.rb ┣ doc ┃ ┗ fizzbuzz.md ┣ lib ┃ ┗ fizzbuzz.rb ┗ spec ┗ fizzbuzz_spec.rb
生成されたファイルの確認
doc/fizzbuzz.md
# Fizzbuzz class ## fizzbuzz method pending
lib/fizzbuzz.rb
# encoding: utf-8 class Fizzbuzz def fizzbuzz # TODO: implement your code end end
spec/fizzbuzz_spec.rb
# encoding: utf-8 require "spec_helper" require "fizzbuzz" describe Fizzbuzz do context :fizzbuzz do cases = [ { case_no: 1, case_title: "case_title", expected: "expected", }, ] cases.each do |c| it "|case_no=#{c[:case_no]}|case_title=#{c[:case_title]}" do begin case_before c # -- given -- fizzbuzz = Fizzbuzz.new # -- when -- # TODO: implement execute code # actual = fizzbuzz.fizzbuzz # -- then -- # TODO: implement assertion code # expect(actual).to eq(c[:expected]) ensure case_after c end end def case_before(c) # implement each case before end def case_after(c) # implement each case after end end end end
fizzbuzzを実装します。
$fizzbuzz examples start$
と
$fizzbuzz examples end$
は後でAPIコメントを差し込める箇所とするための規約です。
./lib/fizzbuzz.rb
# encoding: utf-8 class Fizzbuzz # FizzBuzz # # === Example # $fizzbuzz examples start$ # $fizzbuzz examples end$ # def fizzbuzz(count) ret = [] (1..count).each do |c| if c % 15 == 0 ret << 'fizzbuzz' elsif c % 3 == 0 ret << 'fizz' elsif c % 5 == 0 ret << 'buzz' else ret << c end end ret end end
./spec/fizzbuzz_spec.rb
# encoding: utf-8 require "spec_helper" require "fizzbuzz" describe Fizzbuzz do context :fizzbuzz do cases = [ { case_no: 1, case_title: "sample case", input: 15, expected: [1, 2, "fizz", 4, "buzz", "fizz", 7, 8, "fizz", "buzz", 11, "fizz", 13, 14, "fizzbuzz"], }, ] cases.each do |c| it "Fizzbuzz.new.fizzbuzz(#{c[:input]}) # => #{c[:expected].to_s}" do begin case_before c # -- given -- fb = Fizzbuzz.new # -- when -- actual = fb.fizzbuzz(c[:input]) # -- then -- expect(actual).to eq(c[:expected]) ensure case_after c end end def case_before(c) # implement each case before end def case_after(c) # implement each case after end end end end
テスト実行
$ rspec -fd Run options: include {:focus=>true} All examples were filtered out; ignoring {:focus=>true} Fizzbuzz fizzbuzz Fizzbuzz.new.fizzbuzz(15) # => [1, 2, "fizz", 4, "buzz", "fizz", 7, 8, "fizz", "buzz", 11, "fizz", 13, 14, "fizzbuzz"] Finished in 0.002 seconds 1 example, 0 failures
generator.rb update_api_doc の動作検証
$ ruby bin/generator.rb update_api_doc fizzbuzz
update_api_docによって更新された ./lib/fizzbuzz.rb(抜粋)
メソッドヘッダーにテストケースの例が差し込まれました。
# FizzBuzz # # === Example # $fizzbuzz examples start$ # Fizzbuzz.new.fizzbuzz(15) # => [1, 2, "fizz", 4, "buzz", "fizz", 7, 8, "fizz", "buzz", 11, "fizz", 13, 14, "fizzbuzz"] # $fizzbuzz examples end$ #
update_api_docによって更新された ./doc/fizzbuzz.md
# Fizzbuzz class ## fizzbuzz method ~~~ruby Fizzbuzz.new.fizzbuzz(15) # => [1, 2, "fizz", 4, "buzz", "fizz", 7, 8, "fizz", "buzz", 11, "fizz", 13, 14, "fizzbuzz"] ~~~
さらにテストケースを一つ増やしてみます。
./spec/fizzbuzz_spec.rb(抜粋)
{ case_no: 2, case_title: "sample case2", input: 10, expected: [1, 2, "fizz", 4, "buzz", "fizz", 7, 8, "fizz", "buzz"], },
ケースが増えたので、api documentも更新してみます
$ ruby bin/generator.rb update_api_doc fizzbuzz
update_api_docによって更新された ./lib/fizzbuzz.rb(抜粋)
メソッドヘッダーにテストケースの例が更新されました。
# FizzBuzz # # === Example # $fizzbuzz examples start$ # Fizzbuzz.new.fizzbuzz(15) # => [1, 2, "fizz", 4, "buzz", "fizz", 7, 8, "fizz", "buzz", 11, "fizz", 13, 14, "fizzbuzz"] # Fizzbuzz.new.fizzbuzz(10) # => [1, 2, "fizz", 4, "buzz", "fizz", 7, 8, "fizz", "buzz"] # $fizzbuzz examples end$ #
update_api_docによって更新された ./doc/fizzbuzz.md
# Fizzbuzz class ## fizzbuzz method ~~~ruby Fizzbuzz.new.fizzbuzz(15) # => [1, 2, "fizz", 4, "buzz", "fizz", 7, 8, "fizz", "buzz", 11, "fizz", 13, 14, "fizzbuzz"] Fizzbuzz.new.fizzbuzz(10) # => [1, 2, "fizz", 4, "buzz", "fizz", 7, 8, "fizz", "buzz"] ~~~