はじめに
非同期な振る舞いを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ライブラリを利用して、タイムアウトを検出する。
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ライブラリを利用して、複数テストを書く。
あまり、ベストな方法ではない気がするが、こんなようにしてみた。
- スクリプト風に書いていたものをクラスに置き換える。
- 実行したいテストケースごとにメゾッドを作成して、テストスイートから順次呼び出す
- 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
終わりに
実際にテストできるかどうかはまだまだ試行錯誤が必要そうだ。実際の仕事で適用するためには、後処理から正常な状態に復旧させる処理が最大の課題だと思っている。あとメンテナンスも。
なんとなく、可能性だけはつかめた気がしたので、いろいろと隠れて遊んでみようと思う。