Edited at

MochaとMinitestでRubotyの自動テスト

More than 1 year has passed since last update.

Minitestの勉強も兼ねてRubotyの自動テストを作ってみました。

MinitestもRubotyの自動テストについても情報が少なく四苦八苦しましたが、

モックライブラリMochaを使ってやりたいことが実現できたので、やったことを記録します。

Rubotyをどうテストするのか、Mochaの使い方がメインの話です。

Ruboty自体やその構成要素のHandler、Actionについての説明は以下が詳しいです。

なおテストコードは個人的な好みでMinitestのSpecスタイルで書いていますが、基本的にはUnitTestスタイルでも同じことができるはずです。


:golf: ゴールと方針


  • Handler


    • Handlerの呼び出しはRuboty::Robot#receive経由で呼び出す。

    • Actionへの振り分けが責務なので、Actionが呼ばれること/呼ばれないことを確認する。

    • Actionが呼ばれる場合


      • 受け取ったメッセージの正規表現マッチを使用することが多いので、正規表現マッチが想定通り取得できていることも確認したい。


      • :arrow_right: 呼び出しの検証にはモックを使う。


      • :arrow_right: 正規表現マッチはAction.newの引数のmessageに格納されているので、Action.newをスタブにして引数を取り出す。



    • Actionが呼ばれない場合



      • :arrow_right: Action#callをモックにして、呼び出されないことを検証する。





  • Action


    • Handlerから受け取ったRuboty::Messageに格納している正規表現マッチに基づいた処理分岐ができることを確認する。



      • :arrow_right: Ruboty::Message#[]をスタブ化して、Actionのインプットとなる正規表現マッチを自由に設定できるようにする。


      • :arrow_right: Ruboty::Message#replyをモック化し、Actionの結果検証する。






:scroll: テスト対象

テスト対象のソースはruboty-genで生成できるソースを元に、テストのバリエーションを増やすために少し手を加えたものを使用します。

# Usage: ruboty-gen gem アプリ名 アクション名

ruboty-gen gem minitest hello


Handler


lib/ruboty/handlers/minitest.rb

require 'ruboty/minitest/actions/hello'

module Ruboty
module Handlers
# handler
class Minitest < Base
on(/minitest +(?<hello>hello) +(?<target>.+)/, name: 'hello', description: 'helloと言う')

def hello(message)
Ruboty::Minitest::Actions::Hello.new(message).call
end
end
end
end



  • 正規表現部分を変更。


Action


lib/ruboty/minitest/actions/hello.rb

module Ruboty

module Minitest
module Actions
# hello
class Hello < Ruboty::Actions::Base
def call
if message[:target] == 'me'
message.reply(message[:hello])
else
message.reply("#{message[:hello]} #{message[:target]}")
end
end
end
end
end
end


  • 正規表現マッチで分岐するように変更。


:pencil: テストコード


Handler Test


test/ruboty/handlers/minitest_test.rb

require 'test_helper'

describe Ruboty::Handlers::Minitest do
let(:robot) { Ruboty::Robot.new }

before do
end

describe '#hello' do
let(:action) { Ruboty::Minitest::Actions::Hello }

# Action#callの代わりとなるmockを作る(1回呼ばれるかを検証)
let(:mock_action_call) { mock().tap { |mock| mock.expects(:call).once } }

it 'Actionが呼ばれることの検証' do
# Action.newの引数のmessageを取り出して、正規表現のマッチが正しいかを検証する
action
.stubs(:new)
.with do |message|
message[:hello].must_equal 'hello'
message[:target].must_equal 'world'
end
.returns(mock_action_call)

robot.receive(body: 'ruboty minitest hello world', from: 'sender', to: 'channel')
end

it 'Actionが呼ばれないことの検証' do
# Action#callをmock化して、呼ばれないことを検証
action.any_instance.expects(:call).never
robot.receive(body: 'ruboty minitest never', from: 'sender', to: 'channel')
end
end
end



  • 「Actionが呼ばれることの検証」でやっていること


    • Handlerはメッセージがマッチした場合、 Hello.new(message).call を実行する。

    • 検証したいことは以下の通り。


      • Helloをnewした後にcallが呼ばれること。

      • Hello.newの引数のmessageに格納されている正規表現マッチが想定通りか。



    • Hello.newは引数以外は検証する必要がないためスタブ化してしまい、引数を.withで抜き出すことで、上記2を検証。

    • Hello.newの後のcallが呼ばれることは検証したいため、Hello.newのスタブの戻り値として、モックを指定し、onceで上記1を検証。

    • なお、Mochaのmockはテスト終了時に自動で検証を実行するため、verifyなどを呼ぶ必要はない。



  • 「Actionが呼ばれないことの検証」でやっていること


    • Hello#callが呼ばれないことだけ検証したいので、any_instanceで#callをモック化。neverで一度も呼ばれていないことを検証。




Action Test


test/ruboty/minitest/actions/hello_test.rb

require 'test_helper'

describe Ruboty::Minitest::Actions::Hello do
subject { Ruboty::Minitest::Actions::Hello.new(mock_message) }

let(:mock_message) { mock }

before do
end

describe '#call' do
it '<target>が"me"の場合、helloだけ返す' do
# スタブで<hello>と<target>を設定
mock_message.stubs(:[]).with(:hello).returns('hello')
mock_message.stubs(:[]).with(:target).returns('me')
# モックでreplyの引数を検証
mock_message.expects(:reply).with('hello').once

subject.call
end

it '<target>が"me"以外の場合、hello worldを返す' do
# スタブのreturnsをカンマ区切りにした場合、アクセスした順番に値を返す
mock_message.stubs(:[]).returns('world', 'hello', 'world')
# モックでreplyの引数を検証
mock_message.expects(:reply).with('hello world').once

subject.call
end
end
end



  • 「targetが"me"の場合、helloだけ返す」でやっていること


    • Action#callでは処理のインプットとして、Action.newした際の引数のmessageを使用するため、newに渡すモックを作る。

    • インプットのために、スタブとして[]メソッドを用意する。



      • message[:hello]では'hello'を返す。


      • message[:target]では'me'を返す。



    • Action#callで最終的に実行されるmessage.replyを検証するために、モックとしてreplyメソッドを定義する。



      • with()で引数を検証。


      • onceで呼び出し回数を検証。





  • 「targetが"me"以外の場合、hello worldを返す」でやっていること


    • 基本的には"me"の時と同じ。

    • 備忘のために、スタブでのreturnsの指定の仕方を変えてみた。



      • withで引数の条件を指定するのではなく、カンマ区切りで複数指定することで[]を呼び出した順番に値を返している。






:globe_with_meridians: 参考


:bulb: まとめ

Mochaがとてもとても優秀。

MinitestやRubotyに限ったものでもないので、どんどん使っていきたい。

GitHubにソース一式あげています。

zeero/work-ruboty-minitest