Sysadmin Toolbox

第2回Perlによるテストの自動化

管理者の重要な仕事のひとつに、テストがあります。新しいシステムのテスト、H/W、 OSやソフトウェアなどの移行にともなうテスト、過去に発生したミスの再発防止テストなどいろいろなテストありますが、目的はシステムが要件通りに稼働し続けることの保証です。今回は、テストの自動化を取り上げます。

テストの自動化

安定したシステムにテストは欠かせませんが、みなさんの環境ではどのようにテストをしているでしょうか。手順がExcelに並べられているだけだったりしませんか。ミスが起きたら、再度同じミスをしないためのテストをしていますか。ミスが起きない、もしくはミスがあってもできるだけ迅速に検知できることをテストで保証できているでしょうか。

以前ファイアーウォールの移行作業をしたのですが、製品のベンダが異なるためACLの記法も異なり、設計思想も異なっていため、簡単にはACLを移行することができませんでした。ACLも1,000のオーダーに達していたため、作業中に発生するミスを考えると、ちまちま手作業で移行することも無理でした。導入にあたってはベンダによるサポートがありましたが、彼らの方法論は「ExcelにACLをコピーして、手作業で書換える」という、ユーザとしては受け入れ難いものでした。

さて、この移行作業をどうテストすればよいでしょうか。手作業にはミスがつきものですから、ACLは自動で変換したいところです。また、変換したACLに間違いがないかを事前に検証したいですね。ACLは内部のサブネットごとに異っていたので、各サブネットから移行前と移行後で同じACLが適用されていることを、事前に検証したいところです。

いろいろあったものの、最終的にはPerlによる変換スクリプトと独自のテストスクリプトを作成し、ほぼ上記の目的は達成し、移行することができました。これは、前述したモジュールがなければまず無理な作業でした。現場ではさまざまな要件がでてきますが、困ったときでもなんとかそれを可能にしてくれるPerlのモジュールが、Perlを管理者にとってかけがえのない言語にしていると思わせる一件でした。

本題に戻ってテストの自動化です。人為的ミスという言葉がありますが、人間はミスをするものですから人為的ミスを前提にテストしなければいけません。なくすことは無理であっても、人為的ミスが入り込む余地をテストで極力減らす必要があります。それなのに、そのためのテストが手動であってはいけません。ですから、テストは自動化するべきです。

なぜPerlなのか

以前は、Perlならだいたいのホストにインストールされていると言われたものでしたが、いまやRuby、Pythonといった言語もデフォルトでインストールされていることは珍しくありません。では、管理者がPerlを使うメリットとはなんでしょう。

Perlの最大の強みのひとつは、その膨大な数のモジュールであると言われます。PerlのモジュールはCPANと呼ばれるリポジトリで公開されており、執筆時点で6,000人以上の開発者が12,000を超えるモジュールを公開しています。これらのモジュールには、管理者のために管理者によって作成されたモジュールもたくさん含まれています。⁠怠惰」は管理者の美徳のひとつですから、すでに公開されている既存の資産を利用しない手はありません。

CPANで公開されているモジュールを再利用するメリットはそれだけではありません。こうしたモジュールは不特定多数によってさまざまな環境で使われますので、ある程度の品質を期待できます。少なくとも、外の世界と隔絶され、誰にもレビューされることのない環境で書かれたプログラムよりは品質が高いと期待できます。品質の高いモジュールを使えば、全体のコードの品質があがるというわけではありませんが、同じ程度にダメなプログラムなら、壊れた独自のライブラリによって書かれたプログラムより、CPANのモジュールを使って使っているプログラムの面倒を見る方がまだましです(往々にして、管理者によって書かれたプログラムは、ドキュメントなし、コメントなし、書いた本人はいない、ということが多いですから⁠⁠。

CPANモジュールの使いかた

CPANで公開されているモジュールは、cpanというCPANのパッケージ管理システムでインストールすることができます。ただし、Mac OS XをのぞくUnix-like OSのディストリビューションでは、それぞれのパッケージ管理システムでCPANのモジュールを管理できますし、ディストリビューション固有のサポートも受けられますので、ディストリビューションのパッケージ管理システムから利用するのがお勧めです。例えば、Gentoo/Linuxではg-cpanというコマンドで、Portageに存在しないモジュールでも自動的にebuildを作成して、Portageからインストールしたかのように管理できます。詳しくは、お使いのディストリビューションのマニュアルを参照してください。

インストールしたモジュールのマニュアルはperldocコマンドで参照できます。Foo::Barというモジュールならperldoc Foo::Barで参照できます。モジュールのソースコードもperldoc -m Foo::Barといったように、コマンド1つで見ることができます。

モジュール紹介

管理者にとって便利なモジュールをいくつか紹介します。

Net::Ping, Net::Ping::External
ホストの到達性をテストするモジュールです。ICMPだけでなく、TCPやUDPを使うテストも可能です。
Net::DNS, Net::LDAP, Net::SMTP, Mail::POP3Client, libwww-perl
それぞれ、各種アプリケーション層のプロトコルを利用するためのモジュールです。
Net::DHCP, Net::BGP, Net::SNMP
L2/L3機器を監視したり、保持しているデータを処理するのに便利なモジュールです。
Net::IP, Net::Netmask, NetAddr::IP, Net::MAC::Vendor
IPアドレスの処理や操作には欠かせないモジュールです。
Net::Telnet, Net::SSH, Net::SSH::Expect, Net::SSH::Perl
実際にコンソールにアクセスしなければできない操作をPerlから実行するのに便利なモジュールです。
Net::Pcap, Net::Packet
より低レベルのネットワーク通信にアクセスを提供するモジュールです。
Nmap::Parser, Nagios::Object, POE::Filter::Snort
管理者におなじみのツールにPerlでアクセスできるモジュールです。

いかがでしょう。少しの想像力とこうした強力なモジュールがあれば、大体のことは自動化することができます。

初めてのテスト

ここではPerlの文法などの解説はしませんが、そんなに複雑なプログラムではありませんので、shが理解できれば雰囲気はつかんでいただけると思います。使用しているPerl のバージョンは5.8.8ですが、Perl 5.6以降であれば動作するはずです。

シンプルなテストを作成してみましょう。ここでは、もっとも基本的なテストモジュールのTest::Simpleを使います。まず、作業ディレクトリを作成しましょう。そして、エディタで次のテストを作成し、t/01.tとして保存します。

> mkdir mytest
> cd mytest
> mkdir t
> vim t/01.t

#!/usr/bin/perl
use strict;
use warnings;

use Test::Simple tests => 1;

ok( 1 + 1 == 2, '1 + 1 = 2');

さっそく実行してみましょう。

> perl t/01.t
1..1
ok 1 - 1 + 1 = 2

テストはパスしたようです。では、順番に解説していきます。

use Test::Simple tests => 1;

Test::Simpleというモジュールを使い、1回のテストを実行すると宣言しています。このテストプランの数値とテストの個数が一致していないと警告が出ます。

ok( 1 + 1 == 2, '1 + 1 = 2');

ok()Test::Simpleの関数で、ok( EXPR, $description )EXPRが真であればテストが成功したとみなし、偽であればテストは失敗です。第2引数は必須ではありませんが、どのテストが失敗したかがわかるような簡潔な説明を指定します。

次に、実際にある関数をテストしてみます。次のテストを作成し、t/02.tとして保存します。

> vim t/02.t

#!/usr/bin/perl
use strict;
use warnings;

use Test::Simple tests => 1;

sub is_integer {
  my ($number) = @_;
  if ( $number =~ qr/^\d+/ ) {
    return 1;
  }
  else {
    return;
  }
}
ok( is_integer('1'), '1 is integer' );

is_integer()がテスト対象の関数です。正規表現を使い、引数が正の整数かどうかを判定し、正の整数なら真、そうでなければ偽を返します。テストを実行します。

> perl t/01.t
1..1
ok 1 is integer

これだけではテストとして不十分ですので、さらにテストを追加してみましょう。正の整数を与えた場合に真を返すという動作だけでなく、正の整数ではない引数を与えた場合に偽を返すという動作もテストします。t/02.tに以下のテストを追加します。

ok( is_integer('1000'), '1000 is integer' );
ok( !is_integer('ABC'), 'ABC is not integer' );
ok( !is_integer('1.1'), '1.1 is not integer' );

テストを追加したぶんだけテストプランの数も、use Test::Simple tests => 4;というように増やします。実行するとどうなるでしょうか。

> perl t/02.t
1..4
ok 1 - 1 is integer
ok 2 - 1000 is integer
ok 3 - ABC is not integer
not ok 4 - 1.1 is not integer
#   Failed test '1.1 is not integer'
#   at t/01.t line 20.
# Looks like you failed 1 test of 4.

最後のテストが失敗しています。1.1は整数ではないのに真が返っています。原因は次の部分にありました。

if ( $number =~ qr/^\d+/ ) {
  return 1;
}
else {
  return;
}

正規表現で先頭から1個以上の数字が連続する場合に真を返していますが、この正規表現は"1.1""111foo"にもマッチしてしまいます。正しくは、先頭から最後まで1個以上の数字が連続する、でなければなりません。正しい正規表現は次のようになります。

if ( $number =~ qr/^\d+$/ ) {
...

修正したテストを実行すると、今度はすべてのテストがパスします。

> perl t/02.t
1..4
ok 1 - 1 is integer
ok 2 - 1000 is integer
ok 3 - ABC is not integer
ok 4 - 1.1 is not integer

複数のテストをまとめて実行する

テストが増えてくると、いままでのように1つずつ実行していられません。まとめて複数のテストを実行できると便利です。そのためのコマンドがproveです。

> prove t
t/01....ok
t/02....ok
All tests successful.
Files=2, Tests=5,  0 wallclock secs ( 0.04 cusr +  0.01 csys =  0.05 CPU)

すべてのテストがパスした場合は、このようにサマリだけが表示され、テストの説明などは表示されなくなります。

t/01....ok
t/02....ok 1/4
#   Failed test '1.1 is not integer'
#   at t/02.t line 20.
t/02....NOK 4/4# Looks like you failed 1 test of 4.
t/02....dubious
        Test returned status 1 (wstat 256, 0x100)
DIED. FAILED test 4
        Failed 1/4 tests, 75.00% okay
Failed Test Stat Wstat Total Fail  List of Failed
-------------------------------------------------------------------------------
t/02.t         1   256     4    1  4
Failed 1/2 test scripts. 1/5 subtests failed.
Files=2, Tests=5,  0 wallclock secs ( 0.05 cusr +  0.00 csys =  0.05 CPU)
Failed 1/2 test programs. 1/5 subtests failed.

テストが失敗した場合は、どのテストで失敗したのかが表示されます。

モジュールを使う

ネットワークに関連するモジュールを実際に使ったプログラムを書いてみましょう。ここでは、もっとも基本的なモジュール、Net::Ping::Externalを使います。cpanもしくはお使いのパッケージ管理システムでNet::Ping::Externalをインストールします。

Net::Ping::Externalの使いかたは簡単です。以下のプログラムをt/ping.plとして保存します

> vim t/ping.pl

#!/usr/bin/perl
use strict;
use warnings;
use Net::Ping::External qw{ ping }; # Net::Ping::Externalのping()を使う

my $host = '127.0.0.1';             # 対象のIPアドレス
my $result = ping( host => $host ); # pingを実行
if ( $result ) {                    # 結果が真なら
  print "$host is alive\n";         # 返事あり
}

実に簡単ですね。実行してみます。

> perl t/ping.pl
127.0.0.1 is alive

モジュールのマニュアルはperldocで参照できます。タイムアウトやリクエストの数などを指定することも可能です。詳しくはperldoc Net::Ping::Externalを参照してください。

モジュールを使ってテストを作る

先ほどのサンプルはまだテストとは言えません。Test::Simpleを使って、テストを作成してみましょう。ping()はホストから返事があれば真、返事がなければ偽を返しますから、そのままok()で使えます。以下のプログラムをt/ping01.tとして保存します。

#!/usr/bin/perl
# ping01.t

use strict;
use warnings;
use Test::Simple tests => 1;
use Net::Ping::External qw{ ping };

my $host = '127.0.0.1';
ok( ping( host => $host ), "$host is alive" );

複数のホストをテストするのも簡単です。

#!/usr/bin/perl
# ping02.t

use strict;
use warnings;
use Test::Simple tests => 2;
use Net::Ping::External qw{ ping };

my @hosts = qw( 127.0.0.1 192.168.0.1 );
for my $host ( @hosts ) {
  ok( ping( host => $host ), "$host is alive" );
}

テストモジュールを作る

ここまでで、基本的なテストの書き方がわかれば、テストケースの作成はそんなに難しいものではないことが理解できたと思います。さて、同じようなテストを繰り返すことが増えてきたら、独自のモジュールを作成してみましょう。

シンプルなテストであれば、前述したテストのように既存のモジュールを利用してテストは書けます。ですが、より複雑なテストを書く場合は、テストの記述が煩雑になり、複数のテストで同じ記述を繰り返すことになります。また、サイト固有の要件などもモジュール化してしまえば、各テストで共有できミスの削減や省力化につながります。

初めてのテストモジュール

ここでは、先ほどのpingによるテストを例にして、モジュールを作成します。テストモジュールを作成するといっても、とりわけ難しいものではありません。Test::Builderモジュールが、テストモジュールを作るための枠組を提供してくれます。次のプログラムをt/lib/Mydomain/Test/Net/Ping/External.pmとして保存します 。

> mkdir -p t/lib/Mydomain/Test/Net/Ping
> vim t/lib/Mydomain/Test/Net/Ping/External.pm

use strict;
use warnings;

use base 'Exporter';
our @EXPORT = qw{ ping_ok };

use Test::Builder;
my $test = Test::Builder->new;

use Net::Ping::External qw{ ping };

sub ping_ok {
  my ( $ip, $desc ) = @_;
  return (   $test->ok( ping( host => $ip), $desc )
          || $test->diag("\t$ip doesn't respond") );
}
1;

順番に解説します。

use base 'Exporter';
our @EXPORT = qw{ ping_ok };

これはExporterモジュールを使い、他のプログラムからping_okという関数をあたかもそのプログラムで定義されているかのように呼び出すためのおまじないです。

use Test::Builder;
my $test = Test::Builder->new;

ここでTest::Builderモジュールを使い、あたらしいTest::Builderオブジェクトを作成しています。

use Net::Ping::External qw{ ping };

先程のテストと同じように、Net::Ping::Externalを使います。

sub ping_ok {
  my ( $ip, $desc ) = @_;
  return (
    $test->ok( ping( host => $ip), $desc ) ||
    $test->diag( "\t$ip doesn't respond" )
  );
}

これがこのモジュールのキモです。ping_okは、呼び出された引数を受け取り、テスト(ここではping( host => $ip )を実行し、失敗した場合はメッセージを表示しています。ping_ok()の引数は、IP addressとテストの短い説明の$descの2つです。

return ( $test->ok( EXPR, $desc ) || $test->diag(@mesg) );

がひとつのイディオムです。EXPRには真偽値を返すテスト条件を書きます。

では、作成したテストモジュールをテストから利用してみましょう。以下のプログラムをt/ping02.tとして保存します。

#!/usr/bin/perl
use strict;
use warnings;

use Test::More tests => 1;
use lib 't/lib';
use Mydomain::Test::Net::Ping::External;
ping_ok('127.0.0.1', '127.0.0.1 is alive');

use libで、利用するモジュールのパスを指定しています。テストを実行しているパスからの相対ディレクトリです。use Mydomain::Test::Net::Ping::External;でテストに使用するモジュールを指定しています。

実行してみましょう。

> perl t/ping02.t
1..1
ok 1 - 127.0.0.1 is alive

失敗した場合は次のように出力されます。

> perl t/ping02.t
1..1
not ok 1
#   Failed test at t/ping02.t line 9.
#       127.0.0.2 doesn't respond
# Looks like you failed 1 test of 1.

このようにTest::Builderを使って作成したテストモジュールは、他のTest::*モジュールと共存できます。例えば、proveを実行してもきちんと表示されます。

> prove t
t/01........ok
t/02........ok
t/ping01....ok
t/ping02....ok
All tests successful.
Files=4, Tests=7,  0 wallclock secs ( 0.09 cusr +  0.02 csys =  0.11 CPU)

Test::Builderの詳しい説明はperldoc Test::Builderを参照してください。

これで、ルータやファイアーウォールの設定を変更したりしても、通信の確認を自動的に行うことができるようになります。実際には、テスト環境と本番環境では対象のホス トが異なったり、複数のサブネットから疎通確認をしたい、といった要件が発生します が、これは読者への宿題とすることにします。

さらなるテスト

今回はpingを対象にテストを作成しました。けれども、管理者が行うテストはまだまだあります。サービスの正常稼働をテストするにしても、単にサービスの反応があればいいわけではありません。

  • ユーザがログインできて、期待する反応が返ってくるか(複数の処理のテスト)
  • 外部からメールを送信したら、宛先のメールボックスに配送されて、そのメッセージを取り出せるか(複数のサービスのテスト⁠
  • 許可されたユーザが許可されているサービスにアクセスでき、そうではないユーザはアクセスできないか(ACLの確認)
  • 異常な入力やアクセスにも適切に対応できるか(egress filtering/open relay/open proxy/open DNS server)
  • 過去に起きたミスが繰り返されていないか(ミスのテストケース化)

こうしたテストも今回作成したテストの延長線上にあります。また、作成したテストを Nagiosなどの監視ツールから実行して、より高度なサービス監視も可能です。

まとめ

Perlは管理者にとって有用なツールです。これまでに多くの管理者によって使われており、数多くの管理者に有用なモジュールが利用できます。これらのモジュールを使いこなすことができれば、困難な課題も解決が可能になります。ミスを防止するためのテストなのですから、テストは自動化すべきです。テストのフレームワークを利用すると、テストモジュールを比較的簡単に作成することができます。テストモジュールを作成すると、複数のテストで再利用ができます。

まずは既存のテストを見直して、単純なテストや繰り返されるテストを自動化しましょう。自動化することで、誰でもテストが実行できるようになり、短時間で確実にテストが行えるようになります。

今回紹介したのはソフトウェア開発向けに作られたテストのフレームワークですが、管理者にも有益なツールです。独自のスクリプトをスクラッチから作成して車輪を再発明するのではなく、再利用性や汎用性をもっと考慮すべきです。この他にも、ソフトウェア開発から管理者はたくさんのことを学べるでしょう。

References

おすすめ記事

記事・ニュース一覧