Upgrade to Pro — share decks privately, control downloads, hide ads and more …

minitest に学ぶメタプログラミング / Learn meta programming from minitest

muryoimpl
February 01, 2020

minitest に学ぶメタプログラミング / Learn meta programming from minitest

2020-02-01 に開催された BuriKaigi 2020 の Room C での発表です。

muryoimpl

February 01, 2020
Tweet

More Decks by muryoimpl

Other Decks in Technology

Transcript

  1. Today’s Contents • minitest とは? • なぜ minitest ? •

    xUnit はどのようなメタプログラミングをしている? • minitest どうなっているのだろうか? • まとめ
  2. minitest とは? • Seattle.rb 製のテスティングフレームワーク ◦ https://github.com/seattlerb/minitest • Ruby on

    Rails でも利用されている • TDD, BDD, mocking, benchmark をサポートしている • 今回は xUnit 形式の Unit test style を主に扱う
  3. 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
  4. なぜ minitest ? • 最近、富山 Ruby 会議01 等で RSpec に絡んだ話をしたの

    で、違うフレームワークも扱わないと(謎の使命感) • 身近な gem である • 読んでみるとあまりコード量が多くない • 「テスティングフレームワーク難しい」というイメージが和らいだ ため
  5. ウォーミングアップ 『テスト駆動開発』に、Python で xUnit テスティングフレームの実 装について書かれている。( 第II部 xUnit ) これを

    Ruby で実装して、xUnit 系の仕組みをみてみた。 https://gist.github.com/muryoimpl/154cbc88796ddcc6e55b 676e6110215e
  6. 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
  7. 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
  8. 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 の例
  9. 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 の例
  10. 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 内での使われ方
  11. 1. Class#inherited Class#inherited を利用して、 class Minitest::Runnable の クラス変数 @@runnables にサブ

    クラスの Class を格納している。 本来サブクラスのことを知らないはずのスーパークラスが サブクラスを保持しているという状態 => Minitest がテスト対象のクラス群を操作する準備ができた
  12. 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.
  13. 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
  14. 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
  15. 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.
  16. 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
  17. 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
  18. まとめ • minitest を題材に、メタプログラミングに使われているメソッド を紹介しました。 • 紹介したのは、Class#inheried, Module#public_instance_methods, Object#send の

    3 つ • minitest はコード量は多くないが、他にも thread, signal handling, diff, plugin 機構など、いろんなことをやっているの で、読んで見てみると勉強になるかもしれません