Share
Embed
minitest に学ぶメタプログラミング / Learn meta programming from minitest
2020-02-01 に開催された BuriKaigi 2020 の Room C での発表です。
muryoimpl
February 01, 2020
Transcript
34 slides
-
Today’s Contents • minitest とは? • なぜ minitest ? •
xUnit はどのようなメタプログラミングをしている? • minitest どうなっているのだろうか? • まとめ -
minitest とは? • Seattle.rb 製のテスティングフレームワーク ◦ https://github.com/seattlerb/minitest • Ruby on
Rails でも利用されている • TDD, BDD, mocking, benchmark をサポートしている • 今回は xUnit 形式の Unit test style を主に扱う -
require "minitest/autorun" class TestMeme < Minitest::Test # <= 基底クラスを継承してテスト用クラス作成 def
setup @meme = Meme.new end def test_that_kitty_can_eat assert_equal "OHAI!", @meme.i_can_has_cheezburger? end def test_that_it_will_not_blend refute_match /^no/i, @meme.will_it_blend? end def test_that_will_be_skipped # <= test から始まるメソッドがテストとして実行される skip "test this later" end end Unit tests https://github.com/seattlerb/minitest#unit-tests -
なぜ minitest ? • 最近、富山 Ruby 会議01 等で RSpec に絡んだ話をしたの
で、違うフレームワークも扱わないと(謎の使命感) • 身近な gem である • 読んでみるとあまりコード量が多くない • 「テスティングフレームワーク難しい」というイメージが和らいだ ため -
ウォーミングアップ 『テスト駆動開発』に、Python で xUnit テスティングフレームの実 装について書かれている。( 第II部 xUnit ) これを
Ruby で実装して、xUnit 系の仕組みをみてみた。 https://gist.github.com/muryoimpl/154cbc88796ddcc6e55b 676e6110215e -
class TestCase # <= テスト用のクラスをつくる基底クラス attr_reader :wasRun, :name, :wasSetUp ---
略 --- def run(result) result.testStarted setUp begin method = @name send(method) # <= self.send(:name) で対象のメソッドを実行する。これだけだった rescue result.testFailed end tearDown end def assert(condition) raise unless condition end end -
minitest v5.14.0 https://github.com/seattlerb/minitest/tree/v5.14.0 テスト実行部分で使っているメタプログラミング要素は 主に 3 つ 1. Class#inherited 2.
Module#public_instance_methods 3. Object#send -
1. Class#inherited サブクラスが定義されたときに、サブクラスを引数に inherited メ ソッドが処理される。 https://docs.ruby-lang.org/ja/latest/method/Class/i/inherited.html 似たような hook に
Module#included, Module#extended があ る。 https://docs.ruby-lang.org/ja/latest/method/Module/i/included.html https://docs.ruby-lang.org/ja/latest/method/Module/i/extended.html -
require 'omniauth/key_store' module OmniAuth class NoSessionError < StandardError; end module
Strategy def self.included(base) OmniAuth.strategies << base base.extend ClassMethods base.class_eval do option :setup, false option :skip_info, false option :origin_param, 'origin' end end module ClassMethods def default_options -- 略 -- end end end https://github.com/omniauth/omniauth/blob/v1.9.0/lib/omniauth/strategy.rb Module#included の例 -
require "minitest/test" class Minitest::Spec < Minitest::Test module DSL module InstanceMethods
def _(value = nil, &block) Minitest::Expectation.new(block || value, self) end -- 略 -- end def self.extended(obj) # :nodoc: obj.send :include, InstanceMethods end end -- 略 -- end https://github.com/seattlerb/minitest/blob/v5.14.0/lib/minitest/spec.rb Module#extended の例 -
module Minitest class Runnable def self.runnables @@runnables end end class
Runnable # re-open def self.inherited klass # サブクラスの class をスーパークラスの # クラス変数に追加する self.runnables << klass super end end end https://github.com/seattlerb/minitest/blob/v5.14.0/lib/minitest.rb Class#inherited の Minitest 内での使われ方 -
1. Class#inherited Class#inherited を利用して、 class Minitest::Runnable の クラス変数 @@runnables にサブ
クラスの Class を格納している。 本来サブクラスのことを知らないはずのスーパークラスが サブクラスを保持しているという状態 => Minitest がテスト対象のクラス群を操作する準備ができた -
Minitest の処理の流れ (1) 実はソースに流れがコメントとして記載されています。 https://github.com/seattlerb/minitest/blob/v5.14.0/lib/minitest.rb#L109-L125 1. Minitest.run が reporter を準備する
2. Minitest.__run を実行する 3. Minitest::Runnable.runnables.each 4. で、Minitest::Test のサブクラスそれぞれの self.run (Minitest::Runnable.run)が呼ばれる 5. Minitest::Test のサブクラスが self.runnable_methods (Minitest::Test.runnable_methods)を呼び、 self.methods_matching (Minitest::Runnable.methods_mathing) が 呼ばれてテストするメソッドを抽出する 1. 2. 3. 4. 5. -
2. Module#public_instance_methods モジュールで定義されている public メソッド名の一覧を配列で返 す。 https://docs.ruby-lang.org/ja/latest/method/Module/i/public_instance_methods.html メソッド文字列の配列を Enumerable#grep で
test から始まるメ ソッド文字列のみ抽出する。 https://github.com/seattlerb/minitest/blob/v5.14.0/lib/minitest/test.rb#L65-L77 https://github.com/seattlerb/minitest/blob/v5.14.0/lib/minitest.rb#L292-L294 https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/grep.html -
module Minitest class Runnable -- 略 -- # 引数 true
は継承したメソッドを含む def self.methods_matching(re) public_instance_methods(true) .grep(re) .map(&:to_s) end -- 略 -- end end Module#public_instance_methods の使われ方 module Minitest class Test < Runnable def self.runnable_methods # test から始まるメソッドを抽出する methods = methods_matching(/^test_/) case self.test_order when :random, :parallel then max = methods.size methods.sort.sort_by { rand(max) } when :alpha, :sorted then methods.sort else raise "Unknown test_order: #{self.test_order.inspect}" end end end end -
Minitest の処理の流れ (2) https://github.com/seattlerb/minitest/blob/v5.14.0/lib/minitest.rb#L109-L125 6. Minitest::Test のサブクラスが self.run_one_method ( Minitest::Runnable.run_one_method)
を 呼び出し、reporter の記録用メソッドと Minitest.run_one_method を呼び出す 7. klass(Minitest::Testのサブクラス)をnewして Minitest::Runnable#initialize(method_name)を呼び 8. klass#run (Minitest::Test#run)を実行する 1. 2. 3. 4. 5. 6. 7. 8. -
Minitest::Test#run module Minitest class Test < Runnable def run with_info_handler
do # コンソールへの出力と signal ハンドリング time_it do # 実行時間の記録 capture_exceptions do before_setup; setup; after_setup # xUnit の hook 系メソッドを実行する self.send(self.name) # 対象のメソッドを実行する end TEARDOWN_METHODS.each do |hook| # before_teardown, teardown, after_teardown capture_exceptions do self.send(hook) # 上のメソッドを順に実行する end end end end Result.from(self) # per contract end end end -
Minitest の処理の流れを確認した Class#inherited で集めたサブクラスに対し、 Method#public_instance_methods で test か ら始まるメソッドを取得して、 サブクラスをインスタンス化して集めたメソッド文
字列を Object#send で実行し、 結果を出力している。 1. 2. 3. 4. 5. 6. 7. 8. -
require "minitest/autorun" class TestMeme < Minitest::Test # <= 基底クラスを継承してテスト用クラス作成 def
setup @meme = Meme.new end def test_that_kitty_can_eat assert_equal "OHAI!", @meme.i_can_has_cheezburger? end def test_that_it_will_not_blend refute_match /^no/i, @meme.will_it_blend? end def test_that_will_be_skipped # <= test から始まるメソッドがテストとして実行される skip "test this later" end end Unit tests https://github.com/seattlerb/minitest#unit-tests -
まとめ • minitest を題材に、メタプログラミングに使われているメソッド を紹介しました。 • 紹介したのは、Class#inheried, Module#public_instance_methods, Object#send の
3 つ • minitest はコード量は多くないが、他にも thread, signal handling, diff, plugin 機構など、いろんなことをやっているの で、読んで見てみると勉強になるかもしれません