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

Ac6dba8ce93944d17714de362ca17e54?s=47 muryoimpl
February 01, 2020

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

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

Ac6dba8ce93944d17714de362ca17e54?s=128

muryoimpl

February 01, 2020
Tweet

Transcript

  1. minitest に学ぶ メタプログラミング 2020/02/01 BuriKaigi2020 @ 富山県民会館 #burikaigi #burikaigiC muryoimpl

  2. self.introduce • 無量井 健 (むりょうい けん) • 永和システムマネジメント所属 • 金沢でリモートワークをしています • Kanazawa.rb

    に出没しています • 代打です
  3. Today’s Contents • minitest とは? • なぜ minitest ? •

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

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

    で、違うフレームワークも扱わないと(謎の使命感) • 身近な gem である • 読んでみるとあまりコード量が多くない • 「テスティングフレームワーク難しい」というイメージが和らいだ ため
  7. xUnit は どんなメタプログラミングをし ている?

  8. ウォーミングアップ 『テスト駆動開発』に、Python で xUnit テスティングフレームの実 装について書かれている。( 第II部 xUnit ) これを

    Ruby で実装して、xUnit 系の仕組みをみてみた。 https://gist.github.com/muryoimpl/154cbc88796ddcc6e55b 676e6110215e
  9. 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
  10. ウォーミングアップ いざ実装してみると… 使ったメタプログラミングっぽいメソッドは Object#send のみでした。 https://docs.ruby-lang.org/ja/latest/method/Object/i/__send__.html

  11. 想像していたより 簡単だった

  12. ウォーミングアップやった結果 対象のメソッドを集めてくる部分は何かしらの仕組みが追加され そうだが • 基本は 基底クラスを継承して • send(:method_name) でテストを実行する が基本になっていた。

  13. minitest は どうなっているの だろうか?

  14. 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
  15. 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
  16. 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 の例
  17. 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 の例
  18. 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 内での使われ方
  19. None
  20. 1. Class#inherited Class#inherited を利用して、 class Minitest::Runnable の クラス変数 @@runnables にサブ

    クラスの Class を格納している。 本来サブクラスのことを知らないはずのスーパークラスが サブクラスを保持しているという状態 => Minitest がテスト対象のクラス群を操作する準備ができた
  21. 余談

  22. クラス変数 (@@variable) クラス変数 はそのクラス変数を持ったクラスを継承した サブクラスでも利用/変更が可能です 思わぬところで値が書き換えられる可能性があるので、 宣言する場合はこの特性に留意して宣言してください!! そのクラスだけに公開する変数を宣言したい場合はクラスインス タンス変数を使いましょう Ruby

    クラスインスタンス変数 [検索]
  23. 余談終了

  24. 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.
  25. 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
  26. 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
  27. テスト対象のクラスと、その検証対象のメソッドが取れた あとは、 レシーバとなるインスタンスに、 検証対象のメソッドを実行させれば、 テストが実行される、 という流れができあがる。 ここで Object#send の出番となる。

  28. 3. Object#send オブジェクトのメソッド名を引数にして呼び出し、メソッドの実行結 果を返す。 https://docs.ruby-lang.org/ja/latest/method/Object/i/__send__.html Minitest::Test のサブクラスと、test から始まるメソッド名を受け 取って実行する。 https://github.com/seattlerb/minitest/blob/v5.14.0/lib/minitest/test.rb#L65-L77

  29. 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.
  30. 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
  31. Minitest の処理の流れを確認した Class#inherited で集めたサブクラスに対し、 Method#public_instance_methods で test か ら始まるメソッドを取得して、 サブクラスをインスタンス化して集めたメソッド文

    字列を Object#send で実行し、 結果を出力している。 1. 2. 3. 4. 5. 6. 7. 8.
  32. 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
  33. まとめ

  34. まとめ • minitest を題材に、メタプログラミングに使われているメソッド を紹介しました。 • 紹介したのは、Class#inheried, Module#public_instance_methods, Object#send の

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