はじめに

非同期な振る舞いをxUnitでテストできたらいいなと思った。具体的には、コンソールからコマンドを打ち込んで標準出力に現れたメッセージをアサートできればいいなと思った。

そういうツールを探してみたのだが、ちょっと探しただけでは見つからなかった。teratermマクロ(TTL)では、コンソールにコマンドを打ち込んでアサートということは簡単に実現できたが、xUnitのような機能、

  • SetUp
  • Teardown
  • テストの実行
  • テスト結果の集計

を自分で実装するのがけっこう大変そうだった。いろいろ調べていると、linuxで標準出力を監視するコマンドでexpectというのがある。これは、RubyやPythonでも同様なツールがあるようだ。

この記事では、GUIはSelenium、CLIはPexpectを利用すれば、どんな自動化テストも可能だと欠かれている。

Quick test automation using Pexpect and Selenium | thoughts from the test eye

そこで今回は、Rubyのminitestライブラリとexpectライブラリを利用して、非同期テストができそうかどうか探ってみた。

TODO

会社では、組み込みソフトの開発をC言語でしているのだが、実機を使ったテストをするときは、実機の目の前に置いてあるゲートウェイPCからシリアルケーブルでボードにアクセスしたり、GUI/CLIをいろいろいじったりしている。

ゲートウェイPCには、Rubyもexpectコマンドもインストールされていないので、自分のCygwinからゲートウェイPCにtelnetして、ウラウラコマンドを送りつけられたらいいなとおもった。なので、今回は以下のようなことを試してみて、実際に実現可能かどうかを探ってみた。

  • サーバにtelnetする。(expect,pty)
  • サーバ上でディレクトリを作成する(mkdir)。
  • ディレクトリが作成できたかどうかをチェックする(lsコマンド)。
  • 複数のテストを実行する。(minitest)
  • タイムアウトを使ってテストの失敗を検出する。(timeout)

サーバにtelnetする

サーバ上でディレクトリを作成する(mkdir)

Rubyを使ってサーバにtelnetしてコマンドを実行する方法は前回記事にしたので、そっちを参照。

Ruby から サーバ上にパスワードなしでtelnetログイン(expect,pty) | Futurismo

#!/usr/bin/env ruby
require 'pty'
require 'expect'

# ログイン情報を入力
hostname="ubuntu"
username="tsu-nera"
password="*******"

# expect で読み込んだ内容を標準出力に出力するおまじない
$expect_verbose=true

PTY.spawn("telnet -l #{username} #{hostname}") do |r,w|
w.sync = true
r.expect(/Password: /) { w.puts "#{password}" }
r.expect(/[$%#]/) { w.puts "mkdir testdir" }
r.expect(/[$%#]/) { w.puts "exit" }
end

ディレクトリが作成できたかどうかをチェックする(lsコマンド)

ちゃんとmkdirでtestdirが作成されたかかを、lsコマンドで画面に表示して確認する。

#!/usr/bin/env ruby 

....

r.expect(/[$%#]/) { w.puts "mkdir testdir" }

r.expect(/[$%#]/){ w.puts "ls" }
r.expect("testdir")

r.expect(/[$%#]/) { w.puts "exit" }
end

しかし、この方法だとtestdirが存在すれば処理が先にすすむのだが、失敗するとずっと待たされる(9999999秒?)。

タイムアウトを使ってテストの失敗を検出する。(timeout)

Rubyのtimeoutライブラリを利用して、タイムアウトを検出する。

library timeout

require ‘timeout’でライブラリを読み込む。タイマを貼りたい場所で、タイムアウトした場合の例外も考慮してて、以下のように書く。

begin

# timeout(タイムアウト値) { 処理 }

rescue

# タイムアウトした時の例外処理

end
#!/usr/bin/env ruby 

require 'timeout'

....

r.expect(/[$%#]/) { w.puts "mkdir testdir" }

r.expect(/[$%#]/){ w.puts "ls" }
begin
    timeout(3) { r.expect("testdir", 5) }
rescue Timeout::Error => ex
      w.puts "exit"
      puts ex.message
      return ex.class
end

r.expect(/[$%#]/) { w.puts "exit" }
end

複数のテストを実行する。(minitest)

ここから、Rubyライブラリのminitest/unitライブラリを利用して、複数テストを書く。

library minitest/unit

あまり、ベストな方法ではない気がするが、こんなようにしてみた。

  • スクリプト風に書いていたものをクラスに置き換える。
  • 実行したいテストケースごとにメゾッドを作成して、テストスイートから順次呼び出す
  • telnet処理はSetUp/Teardownへ移動して複数テストから呼び出せるようにする。

tc_mkdir_expect.rb

$ cat tc_mkdir_expect.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
require 'pty'
require 'expect'
require 'timeout'

# expect で読み込んだ内容を標準出力に出力するおまじない
$expect_verbose=true

class MkdirExpect
  # ログイン情報を入力
  @@hostname="ubuntu"
  @@username="tsu-nera"
  @@password="*******"

  def setup
    # telnet通信を確立
    @pty = PTY.spawn("telnet -l #{@@username} #{@@hostname}")
    @sin = @pty[0]
    @sout= @pty[1]
    @pid = @pty[2]

    @sout.sync=true
    @sin.expect("Password:"){ @sout.puts "#{@@password}" }
  end

  def teardown
    # telnet通信の終了
    @sin.expect(/[$%#]/){@sout.puts "exit" }
  end

  def mkdir_testdir
    # Setup
    @sin.expect(/[$%#]/){
      @sout.puts "test -f testdir || rmdir testdir"
    }

    # Test
    @sin.expect(/[$%#]/){ @sout.puts "mkdir testdir" }

    # Verify
    @sin.expect(/[$%#]/){ @sout.puts "ls" }
    @sin.expect("testdir")

    # teardown
    return 0
  end

  def mkdir_two_directory
    # Setup
    @sin.expect(/[$%#]/){ @sout.puts "test -f dir1 || rmdir dir1" }
    @sin.expect(/[$%#]/){ @sout.puts "test -f dir2 || rmdir dir2" }

    # Test
    @sin.expect(/[$%#]/){ @sout.puts "mkdir dir1" }
    @sin.expect(/[$%#]/){ @sout.puts "mkdir dir2" }

    # Verify
    @sin.expect(/[$%#]/){ @sout.puts "ls" }
    @sin.expect("dir1")
    @sin.expect(/[$%#]/){ @sout.puts "ls" }
    @sin.expect("dir2")

    # teardown
    return 0
  end

  def mkdir_timeout
    # Setup
    @sin.expect(/[$%#]/){
      @sout.puts "test -f testdir || rmdir testdir"
    }

    # Test
    @sin.expect(/[$%#]/){ @sout.puts "mkdir testdir" }

    # Verify
    @sin.expect(/[$%#]/){ @sout.puts "ls" }
    begin
      timeout(3) { @sin.expect("testdir2", 5) }
    rescue Timeout::Error => ex
      @sout.puts "exit"
      puts ex.message
      return ex.class
    end

    # teardown
    return 0
  end
end

次は、テストスイート。これは公式リファレンスを見よう見まねで作成。

ts_mkdir_expect.rb

# -*- coding: utf-8 -*-
require 'minitest/unit'
require 'minitest/autorun'
require './tc_mkdir_expect'

class TestMkdirExpect < MiniTest::Unit::TestCase
  def setup
    @foo = MkdirExpect.new
    @foo.setup
  end

  def teardown
    @foo.teardown
    @foo = nil
  end

  def test_testdir
    assert_equal 0,@foo.mkdir_testdir
  end

  def test_mkdir_two_directory
    assert_equal 0,@foo.mkdir_two_directory
  end

  def test_mkdir_timeout
    assert_equal Timeout::Error,@foo.mkdir_timeout
  end

end

テスト結果

最後に、テストを実行してみる。なかなかよさげだ。

$ ruby ts_mkdir_expect.rb

Finished tests in 37.559148s, 0.0799 tests/s, 0.0799 assertions/s.

3 tests, 3 assertions, 0 failures, 0 errors, 0 skips

テストケースごとにテストを実行することもできる。

$ ruby ts_mkdir_expect.rb -n test_mkdir_timeout

タイムアウトで失敗させると、それなりのメッセージがでる。

Finished tests in 16.746958s, 0.0597 tests/s, 0.0597 assertions/s.

1) Failure:

test_mkdir_timeout(TestMkdirExpect) [ts_mkdir_expect.rb:27]:

Expected: 0

Actual: Timeout::Error

1 tests, 1 assertions, 1 failures, 0 errors, 0 skips

終わりに

実際にテストできるかどうかはまだまだ試行錯誤が必要そうだ。実際の仕事で適用するためには、後処理から正常な状態に復旧させる処理が最大の課題だと思っている。あとメンテナンスも。

なんとなく、可能性だけはつかめた気がしたので、いろいろと隠れて遊んでみようと思う。