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. minitest に学ぶ
    メタプログラミング
    2020/02/01
    BuriKaigi2020 @ 富山県民会館
    #burikaigi #burikaigiC
    muryoimpl

    View full-size slide

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

    View full-size slide

  3. Today’s Contents
    ● minitest とは?
    ● なぜ minitest ?
    ● xUnit はどのようなメタプログラミングをしている?
    ● minitest どうなっているのだろうか?
    ● まとめ

    View full-size slide

  4. minitest とは?
    ● Seattle.rb 製のテスティングフレームワーク
    ○ https://github.com/seattlerb/minitest
    ● Ruby on Rails でも利用されている
    ● TDD, BDD, mocking, benchmark をサポートしている
    ● 今回は xUnit 形式の Unit test style を主に扱う

    View full-size slide

  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

    View full-size slide

  6. なぜ minitest ?
    ● 最近、富山 Ruby 会議01 等で RSpec に絡んだ話をしたの
    で、違うフレームワークも扱わないと(謎の使命感)
    ● 身近な gem である
    ● 読んでみるとあまりコード量が多くない
    ● 「テスティングフレームワーク難しい」というイメージが和らいだ
    ため

    View full-size slide

  7. xUnit は
    どんなメタプログラミングをし
    ている?

    View full-size slide

  8. ウォーミングアップ
    『テスト駆動開発』に、Python で xUnit テスティングフレームの実
    装について書かれている。( 第II部 xUnit )
    これを Ruby で実装して、xUnit 系の仕組みをみてみた。
    https://gist.github.com/muryoimpl/154cbc88796ddcc6e55b
    676e6110215e

    View full-size slide

  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

    View full-size slide

  10. ウォーミングアップ
    いざ実装してみると…
    使ったメタプログラミングっぽいメソッドは
    Object#send のみでした。
    https://docs.ruby-lang.org/ja/latest/method/Object/i/__send__.html

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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 の例

    View full-size slide

  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 の例

    View full-size slide

  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 内での使われ方

    View full-size slide

  19. 1. Class#inherited
    Class#inherited を利用して、
    class Minitest::Runnable の クラス変数 @@runnables にサブ
    クラスの Class を格納している。
    本来サブクラスのことを知らないはずのスーパークラスが
    サブクラスを保持しているという状態
    => Minitest がテスト対象のクラス群を操作する準備ができた

    View full-size slide

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

    View full-size slide

  21. 余談終了

    View full-size slide

  22. 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.

    View full-size slide

  23. 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

    View full-size slide

  24. 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

    View full-size slide

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

    View full-size slide

  26. 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

    View full-size slide

  27. 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.

    View full-size slide

  28. 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

    View full-size slide

  29. Minitest の処理の流れを確認した
    Class#inherited で集めたサブクラスに対し、
    Method#public_instance_methods で test か
    ら始まるメソッドを取得して、
    サブクラスをインスタンス化して集めたメソッド文
    字列を Object#send で実行し、
    結果を出力している。
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.

    View full-size slide

  30. 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

    View full-size slide

  31. まとめ
    ● minitest を題材に、メタプログラミングに使われているメソッド
    を紹介しました。
    ● 紹介したのは、Class#inheried,
    Module#public_instance_methods, Object#send の 3 つ
    ● minitest はコード量は多くないが、他にも thread, signal
    handling, diff, plugin 機構など、いろんなことをやっているの
    で、読んで見てみると勉強になるかもしれません

    View full-size slide