概要
FizzBuzzでストライク・スリーを実装してみる
内容
FizzBuzzでストライク・スリーを実装してみます。
ストライク・スリーについては親記事参照。
仕様
DSLを利用してfizz_buzz_spec.rb を記載すると、
・fizz_buzz_spec.rb をrequireすればプロダクトコードとして利用可能
※FizzBuzzの実クラスはメモリ上に定義されます。
・fizz_buzz_spec.rb をgまたはgenerateオプションを指定して呼ぶとテストコードを生成
・fizz_buzz_spec.rb をdまたはdocオプションを指定して呼ぶとドキュメントを生成
構成
│ fizz_buzz_spec.rb # => DSLを利用して仕様件、テスト件、ソースを書くファイル │ ├─doc │ fizz_buzz.html # fizz_buzz_spec.rbから生成されるドキュメント │ ├─spec # fizz_buzz_spec.rbから生成されるテスト群 │ fizz_buzz_buzz_spec.rb │ fizz_buzz_fizz_buzz_spec.rb │ fizz_buzz_fizz_spec.rb │ spec_helper.rb # ※このファイルは単にrspec --initで生成したもの │ └─strike_three # => DSLの処理本体 strike_three.rb # => DSLスコープ管理用 class_spec.rb # => クラスDSL本体 method_spec.rb # => メソッドDSL本体 test_spec.rb # => テストDSL本体 doc_spec.rb # => ドキュメントDSL本体
ソース
fizz_buzz_spec.rb
# encoding: utf-8 require_relative "./strike_three/strike_three" require "pp" StrikeThree.class_spec(:FizzBuzz, binding).spec do name :FizzBuzz description "FizzBuzzゲーム" method_define.spec do name :fizz args :input desc "入力値が3の倍数の場合のみFizzを返却する" logic <<-EOS input % 3 == 0 ? "Fizz" : "" EOS sample :input => 2, :expected => '', :description => "3で割り切れない場合は空文字を返却" sample :input => 3, :expected => "Fizz", :description => "3で割り切れる場合はFizzを返却" define end method_define.spec do name :buzz args :input desc "入力値が5の倍数の場合のみBuzzを返却する" logic <<-EOS input % 5 == 0 ? "Buzz" : "" EOS sample :input => 4, :expected => '', :description => "5で割り切れない場合は入力値を空文字を返却" sample :input => 5, :expected => "Buzz", :description => "5で割り切れる場合はBuzzを返却" define end method_define.spec do name :fizz_buzz args :input desc <<-EOS 入力値が3の倍数の場合のみFizzを返却する。 入力値が5の倍数の場合のみBuzzを返却する。 入力値が3の倍数かつ5の倍数の場合のみFizzBuzzを返却する。 入力値が3の倍数、5の倍数どちらにも一致しない場合は入力値をそのまま返却する。 EOS logic <<-EOS fb = fizz(input) + buzz(input) fb.empty? ? input.to_s : fb EOS sample :input => 3, :expected => "Fizz", :description => "3で割り切れる場合はFizzを返却" sample :input => 4, :expected => "4", :description => "3,5で割り切れない場合は入力値を返却" sample :input => 5, :expected => "Buzz", :description => "5で割り切れる場合はBuzzを返却" sample :input => 15, :expected => "FizzBuzz", :description => "3,5で割り切れる場合はFizzBuzzを返却" define end doc end
strike_three.rb
# encoding: utf-8 require_relative "./class_spec" class StrikeThree class << self def class_spec(class_name, kernel_scope) @cs = ClassSpec.new(kernel_scope) end end end
class_spec.rb
# encoding: utf-8 require_relative "./method_spec" class ClassSpec attr_accessor :klass, :method_specs, :kernel_scope, :_name, :_description alias :spec :instance_eval def initialize(kernel_scope) @kernel_scope = kernel_scope @method_specs = [] end def name(name) eval "class #{name.to_s};end", kernel_scope @_name = name eval "self.klass = #{name.to_s}.new" end def description(description) @_description = description end def method_define m = MethodSpec.new(@klass) @method_specs << m m end def doc return unless $*.first || ($*.first == "d" || $*.first == "doc") doc = DocSpec.new(self) doc.generate_document end end
method_spec.rb
# encoding: utf-8 require_relative "./test_spec" require_relative "./doc_spec" class MethodSpec attr_accessor :klass , :_name, :_args,:_logic, :test_spec, :doc_spec, :_desc alias :spec :instance_eval def initialize(klass) @klass = klass @test_spec = TestSpec.new(@klass, self) @doc_spec = DocSpec.new(self) end def name(name) @_name = name end def args(args) @_args = args end def desc(desc) @_desc = desc end def logic(logic) @_logic = logic end def sample(options) @test_spec.append(options[:input], options[:expected], options[:description]) end def define create_product_code # gまたはgenerateオプションを指定して実行した場合のみテストクラスを生成 if $*.first && ($*.first == "g" || $*.first == "generate") create_test_code end end private def create_product_code args_str = "" if @_args if @_args.instance_of? Array args_str << "|#{@_args.join(",")}|" elsif @_args.instance_of? Symbol args_str << "|#{@_args.to_s}|" end end @klass.class.class_eval <<-EOS define_method :#{@_name.to_s} do #{args_str} #{@_logic} end EOS end def create_test_code test_spec.generate_test end end
test_spec.rb
# encoding: utf-8 require "erb" require "active_support/core_ext/string" class TestSpec attr_accessor :klass , :_method, :_name, :_samples alias :spec :instance_eval def initialize(klass, _method) @_samples = [] @klass = klass @_method = _method end def append(input, expected, description) @_samples << {:input => input, :expected => expected, :description => description} end def generate_test test_code = create_test_code output_test_file test_code end private def create_test_code template =<<-EOS # encoding: utf-8 require_relative "./spec_helper" require_relative "../<%= spec_path%>" describe <%= class_name%> do describe :<%= method_name%> do <%= method_tests%> end end EOS spec_path = "#{@klass.class.to_s.underscore}_spec" class_name = @klass.class method_name = @_method._name.to_s method_test_ary = [] _samples.each do |s| method_test_ary << " it \"#{s[:description]}\" do" method_test_ary << " actual = #{class_name}.new.#{method_name} #{s[:input]}" method_test_ary << " expect(actual).to eq('#{s[:expected]}')" method_test_ary << " end" method_test_ary << "" end method_tests = method_test_ary.join("\n") erb = ERB.new(template) test_code = erb.result(binding) test_code end def output_test_file(test_code) File.open("./spec/#{@klass.class.to_s.underscore}_#{@_method._name.to_s}_spec.rb", "w") do |f| f.puts test_code end end end
doc_spec.rb
# encoding: utf-8 require 'kramdown' class DocSpec attr_accessor :class_spec def initialize(class_spec) @class_spec = class_spec end def generate_document contents = get_document_contents output_document contents end def get_document_contents doc = [] doc << "# #{class_spec._name}" doc << "" doc << "#{class_spec._description}" doc << "" class_spec.method_specs.each do |ms| args = "" args = ms._args.to_s if ms._args.instance_of? Symbol args = ms._args.join(",") if ms._args.instance_of? Array doc << "## ##{ms._name}(#{args})" doc << "" doc << "#{ms._desc.gsub("\n", "\n\n")}" doc << "" end doc.join("\n") end def output_document(contents) inner_body_html = Kramdown::Document.new(contents.force_encoding('utf-8')).to_html html_template =<<-EOS <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body> %contents% </body> </html> EOS html = html_template.gsub("%contents%", inner_body_html) File.open("doc/#{class_spec._name.to_s.underscore}.html", "w:utf-8") do |f| f.puts html.encode("utf-8") end end end
テストコード
生成コマンド
ruby fizz_buzz_spec.rb g
下記の3ファイルが生成されます。
fizz_buzz_fizz_spec.rb
# encoding: utf-8 require_relative "./spec_helper" require_relative "../fizz_buzz_spec" describe FizzBuzz do describe :fizz do it "3で割り切れない場合は空文字を返却" do actual = FizzBuzz.new.fizz 2 expect(actual).to eq('') end it "3で割り切れる場合はFizzを返却" do actual = FizzBuzz.new.fizz 3 expect(actual).to eq('Fizz') end end end
fizz_buzz_buzz_spec.rb
# encoding: utf-8 require_relative "./spec_helper" require_relative "../fizz_buzz_spec" describe FizzBuzz do describe :buzz do it "5で割り切れない場合は入力値を空文字を返却" do actual = FizzBuzz.new.buzz 4 expect(actual).to eq('') end it "5で割り切れる場合はBuzzを返却" do actual = FizzBuzz.new.buzz 5 expect(actual).to eq('Buzz') end end end
fizz_buzz_fizz_buzz_spec.rb
# encoding: utf-8 require_relative "./spec_helper" require_relative "../fizz_buzz_spec" describe FizzBuzz do describe :fizz_buzz do it "3で割り切れる場合はFizzを返却" do actual = FizzBuzz.new.fizz_buzz 3 expect(actual).to eq('Fizz') end it "3,5で割り切れない場合は入力値を返却" do actual = FizzBuzz.new.fizz_buzz 4 expect(actual).to eq('4') end it "5で割り切れる場合はBuzzを返却" do actual = FizzBuzz.new.fizz_buzz 5 expect(actual).to eq('Buzz') end it "3,5で割り切れる場合はFizzBuzzを返却" do actual = FizzBuzz.new.fizz_buzz 15 expect(actual).to eq('FizzBuzz') end end end
テスト実行結果
$rspec Run options: include {:focus=>true} All examples were filtered out; ignoring {:focus=>true} ........ Finished in 0.007 seconds 8 examples, 0 failures Randomized with seed 35675