組込み開発のシステムテスト・機能テストを自動化できるか?Rubyのminitestで非同期テストを実施する方法を本気出して考えてみた

はじめに

非同期な振る舞いを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= @pty1 @pid = @pty2

@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 mkdirtwodirectory # 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 testtestdir assertequal 0,@foo.mkdir_testdir end

def testmkdirtwodirectory assertequal 0,@foo.mkdirtwodirectory end

def testmkdirtimeout assertequal Timeout::Error,@foo.mkdirtimeout 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

終わりに

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

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