見出し画像

【続】ポンコツ・キャンプ -MiniTestの簡単なツールを作るまでの道程-

ポンコツ・キャンプの続きです。ツール作成の最終投稿になります。

さっそく続きを始めることにします。

まず、はじめに、前回のポンコツ・キャンプでは次の収穫がありました。

  • 配列を集合として演算できること

  • 一般的なオブジェクトのモデルを手にしたこと

  • 各クラス/モジュールのメソッドのシンボルを配列で受け取れること

  • また、各属性から親までの一連の応答メソッドのシンボルを配列で受け取れること

今回のツールを作ろうと思った動機:
assert_respond_toに不満がありました。
これは、定義されたメソッド名をMiniTestでアサートしますが、インスタンス・オブジェクトに対しての応答メッセージのみが検査対象なので、当然、インスタンス・オブジェクトからは、プライベート・メソッドは、見えないのでエラーになってしまいます。プライベート・メソッドでも使えるツールが欲しくなったのです。

想定する使い方:
そこで、前回のポンコツ・キャンプの実行結果:の真ん中のやつを使うことにしました。上のやつを使いたかったのですが、色々な使用状況を考えると難しいので、真ん中のやつで今回は妥協します。

次のような使い方が出来るようにします。

require 'minitest/autorun'
class MyClassTest < Minitest::Test
  include ObjectInterface        # これから実装するメソッドの入っているモジュール

  include ParentInterfaceTest    # Parentクラスのpublic_interface/private_interfaceの入っているモジュール
  include MyModuleInterfaceTest  # MyModuleモジュールのpublic_interface/private_interfaceの入っているモジュール

  def setup
    @my_class = @object = MyClass.new # @objectが開発するメソッドに渡す共有インスタンス
  end

  def test_public_interface
    public_interface(            # このメソッドが実装するメソッド名
      :public_my_class1,         #   テスト対象のメソッド名
      :public_my_class2,         #   テスト対象のメソッド名
      :public_my_class3          #   テスト対象のメソッド名
    )
  end

  def test_private_interface
    private_interface(           # このメソッドが実装するメソッド名
      :private_my_class          #   テスト対象のメソッド名
    )
  end
end

メソッドの中に応答を知りたいメソッド名をシンボルで渡して、お終い。
便利そうじゃない?どお?

Parentクラスは、継承元なので、include ParentInterfaceTestと使うことが出来るようにします。継承が続いても、子供のクラスでインクルードするだけで、再利用するためです。
MyModuleモジュールも同じ理屈です。

今回のオブジェクトのモデルの設定:
モデルのクラスを今回の設定に合わせて変更しておきます。

class Parent
  private;   def private_parent1;  end # メソッド名変更
             def private_parent2;  end # メソッド名変更/追加
  public;    def public_parent1;   end # メソッド名変更
             def public_parent2;   end # メソッド名変更/追加
end

module MyModule
  private;   def private_my_module;   end
  public;    def public_my_module;    end
end

class MyClass < Parent
  include MyModule

  private;   def private_my_class;   end
  public;    def public_my_class1;   end # メソッド名変更
             def public_my_class2;   end # メソッド名変更/追加
             def public_my_class3;   end # メソッド名変更/追加
end

protectedとシングルトンは、理解の簡素化のために割愛しておきました。

ParentInterfaceTestとMyModuleInterfaceTestのモジュールはこうなります。

module ParentInterfaceTest
  def test_public_parent_interface
    public_interface(
      :public_parent1,
      :public_parent2
    )
  end

  def test_private_parent_interface
    private_interface(
      :private_parent1,
      :private_parent2
    )
  end
end

module MyModuleInterfaceTest
  def test_module_public_interface
    public_interface(
      :public_my_module
    )
  end

  def test_module_private_interface
    private_interface(
      :private_my_module
    )
  end
end

test_で始まるメソッド名は、他のモジュールやインクルード先の名称と重複しないようにして下さい。さもないと、メソッドがオーバライドされるからです。

いよいよ、核心。メッソッドの実装です。
現状の把握:
前回のポンコツ・キャンプから、下の式を使いこなすことが出来れば目標が達成できそうな見通しでした。
引数になりそうなものは、obj.classとpublic_instance_methods等の各メソッド名です。あと、public/privateの切り替え用の引数。

print "\nインスタンスobjのParentクラスまでのpublic/private/protected各々の応答できるメッセージ\n\n"
p obj.class.public_instance_methods(true) - Object.public_instance_methods(true)
p obj.class.private_instance_methods(true) - Object.private_instance_methods(true)
p obj.class.protected_instance_methods(true) - Object.protected_instance_methods(true)

実装の方針:
上記の式から得られるのは、実際の応答可能なメソッド名の配列です。これをresponse_setと名付けます。また、利用者の設定するメソッド名の配列をsurvey_setとし、response_setとsurvey_setの積集合はproduct_setと名付けることにします。引数を組み合わせれば、1つのメソッドでいけそうなので、1つのメソッド内で作り上げます。ただ、実際の運用の際に、引数の組合せをごちょごちょやっていては、話しにならないので、各パターンは、ラップに包んで、public_interface/private_interfaceのような使い方が出来るようにして、引数のことを考えなくてもいいようにします。

補足:
理解を深めるためにベン図で表しました。例は、
 response_set = [method1, method2, method3]
 survey_set  = [method1, methodx]
の場合です。

ベン図 参考

実際の応答するメソッドは3つなのに、利用者は2つしか設定しておらず、なおかつ、その1つは、メソッド名が違っています。これは当初、整合性が取れていたはずなのに、途中、メソッドが2つから余分に1つ増えて、メソッド名が変更されてしまい、実際の状況とテストを書いた時の状況に矛盾が生じてしまったと想定してみてください。

このベン図を参考にすると、
 survey_set  - repose_set
 response_set - survey_set
は、それぞれどのような意味を持つのでしょう?

初めの式は、実際のメソッドが存在しない場合です。メソッド名の変更があったのか、typoの可能性を意味します。
次の式は、単純に検証漏れだとか、private属性だと思っていたら、実際はpublic属性になっていて、その上で、public属性を検証していたとかが考えられます。今回は、response_set - survey_setの場合は、実装しません。先のモジュールでインクルードする使い方をした場合、取り扱いが難しくなるからです。

ObjectInterfaceモジュールの実装:
最終的に実装は次のようになります。

module ObjectInterface
  def public_interface(*method_symbols)
    assert_respond_to_methods(:public, @object.class, method_symbols)
  end

  def private_interface(*method_symbols)
    assert_respond_to_methods(:private, @object.class, method_symbols)
  end

  def assert_respond_to_methods(category, obj, survey_set)
    method       = { public: :public_instance_methods, private: :private_instance_methods }[category]
    response_set = obj.send(method, true) - Object.send(method, true)
    product_set  = response_set & survey_set

    assert_empty survey_set - product_set,
                 "not an existing methods in list: #{survey_set - product_set}"
  end
end

簡単に解説すると、assert_respond_to_methodsは、categoryのシンボルから該当するメソッドのシンボルをハッシュから取得し、sendメソッドを使ってシンボルからメソッドを実行しています。最終の検証は、assert_emptyを使って、空集合なのを確認することで、テストをパスさせようとしています。あと、@objectは、モジュールを各種テストで共用するためのインスタンス変数名です。def setup内を参照して下さい。

このツールは、中々有用な働きをしてくれます。
実際にクラス/モジュールを検証していくと、public、privateのリストが夫々に結構長いものが出てきます。そんな時は、内部に密かに別のクラス/モジュールの存在を暗示してくれていたりします。privateは、外部とはメッセージが遮断されていますから、従属するクラス/モジュールが存在するのかもしれません。publicならメッセージ先のオブジェクトの関係の見直しのリストになるはずです。publicのリストは、テストしなければいけないメソッドのリストにも使えます。

いかがでしたか?楽しんでいただけたでしょうか?
是非、ご自身でいじり倒して動作確認することをお勧め致します。

これで、お終いですが、すんなりと解説し過ぎとも思いましたので、課題を提案したいと思います。

課題:

  1. protectedも使用できるように実装されたし。

  2. singletonも使用できるように実装されたし。

  3. module内では、response_set - survey_setの動作は難しいものの自身のクラスのメソッドの検証では有用なため、使い分けができるように実装されたし。

特に、課題3.が実現すれば、安易にattr_accessorを使ってしまい、attr_readerだけで良いものをattr_writerまで実装していたりとか、初期設定値を全部外部に公開してしまうなどの恥ずかしい振る舞いに気が付く可能性が上がります。是非、最後の課題まで、リファクタリングに挑戦してみて下さい。

解答は、投稿しないことにします。障害になることは、ほとんどないはずです。ご自身のやり方で辿りついてみてください。

では、ご健闘を祈ります。

この記事が気に入ったらサポートをしてみませんか?