Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

余談

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

余談終了

Slide 24

Slide 24 text

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.

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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.

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

まとめ

Slide 34

Slide 34 text

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