-
Notifications
You must be signed in to change notification settings - Fork 0
シナリオ実行の流れ
このハンズオンでは、ソフトウェアの受け入れテストツールであるcucumberからNetTesterを連携する形で呼び出し、 ネットワークの構成やテストノードの繋ぎこみをテストに合わせて変更しながら、テストノードからパケットを送信するといった処理を行います。
NetTesterはRubyからライブラリとして、あるいはShellなどからコマンドとして利用できる、 結線制御や仮想ホスト制御を含んだツールです。
NetTesterはテストフレームワーク自体は含んでいません。 RubyやShellから呼ぶ、あるいはRESTで呼ぶことができるのため、既存テストフレームワークと連携可能である、という表現が正確です。
既存テストフレームワークとしては、 cucumber などがあります。
Ruby製の受入テストフレームワークで、試験仕様を日本語で記述できる特徴があります。
日本語でテストを記述する、というのはイメージしにくいかもしませんが、 特定の日本語の文字列に対応するRubyコード(step)を定義しておいて、テストケース(feature)に書かれた日本語に引っかかったstepを実行する、 という仕組みで実現されています。
今回のハンズオン環境のテストシナリオのディレクトリ、ファイルは以下のような構成になっています。
scenario
├── Gemfile [rubyのパッケージ管理ファイル]
├── Gemfile.lock [rubyのパッケージ管理ファイル]
├── Rakefile [タスク定義ファイル]
└── features [シナリオファイルのディレクトリ]
├── *.feature [各シナリオファイル(今回はテストの視点に合わせてuser/、admin/の下に作成)
├── factories.rb [テスト用ネットワーク、テスト用ノード定義ファイル]
├── step_definitions [シナリオで実際に実行されるプログラムコードのディレクトリ]
│ └── *.rb [シナリオで実際に実行されるプログラムコード]
└── support [補助ファイル]
├── aruba.rb [補助ライブラリのファイル]
├── factory_girl.rb [テスト用ネットワーク、テスト用ノード定義を簡単にするライブラリのファイル]
├── hooks.rb [シナリオ実行時、シナリオ終了時のタイミングで行う処理などを記載したファイル]
├── test_node.rb [テスト用ノードクラスのファイル]
└── tester_sets.rb [拠点定義ファイル]
テストの記述と実行の中心となるファイルは、featureファイルとstepファイルです。
featureファイルには、機能に対し、何をしたときにどのような振る舞いをすべきかを記載します。 先述の通り、ここには日本語も記述することができます。
例えば、このシステム(ネットワーク)ではユーザが自分のpcからwebで検索ができるべきである、という機能要件(通信要件)のテストを定義するには、 以下のように記述します。
$ cat features/user/user_pc_to_internet_host/web.feature
Feature: Google 検索
開発者として、
Google で検索したい
なぜなら開発するときによく調べものをするから
Scenario: Web ブラウザで Google を開く
Given 社内 PC
And インターネット上のサーバ
When 社内 PC にログイン
And ブラウザでインターネット上のサーバの Google のページを開く
Then Google のトップページが表示
stepファイルには、featureファイルを実行(Scenario、Given、Andなどの記述が該当)していく際に実際に動作するrubyのコードを記述します。 featureに記載された内容に対するstepは1ファイルである必要はなく、複数のファイルに分ける事が可能で、複数のfeatureから共有して使う事ができます。
後ほど詳しく説明しますが、例えば上記のweb.featureに対するstepは以下のようになっています。
- ホストの定義 (後続stepで使うため、変数にホストを代入) の技術
$ cat features/step_definitions/virtual_host.rb
# coding: utf-8
Given(/^社内 PC$/) do
@user_pc = TestNode.new(attributes_for(:user_pc))
end
-- snip --
Given(/^インターネット上のサーバ$/) do
@internet_host = TestNode.new(attributes_for(:internet_host))
end
-- snip --
- ユーザのアクション (および、対向の制御) の記述
$ cat features/step_definitions/google_steps.rb
# coding: utf-8
When(/^ブラウザでインターネット上のサーバの Google のページを開く$/) do
cd('.') do
@internet_host.exec("rm -f /tmp/index.html; echo '<title>Google</title>' | tee /tmp/index.html", sync: true)
@https_service = @internet_host
@https_service.exec("ruby -rwebrick -rwebrick/https -e 'WEBrick::HTTPServer.new(:DocumentRoot => \"/tmp\", :Port => 443, :SSLEnable => true, :SSLCertName => [[\"CN\", WEBrick::Utils::getservername]] ).start'")
@src_host.exec("sudo mkdir -p /etc/netns/#{@src_host.name}", sync: true)
@src_host.exec("echo '198.51.100.3 www.google.com' | sudo tee /etc/netns/#{@src_host.name}/hosts >/dev/null", sync: true)
@process_id = @src_host.exec('curl -L --insecure https://www.google.com/ | iconv -f SHIFT-JIS -t UTF8', delayed: true)
end
end
- ユーザのアクションの結果の記述
$ cat features/step_definitions/success_steps.rb
-- snip --
Then(/^Google のトップページが表示$/) do
result = @src_host.result(@process_id)
expect(result).to match(/<title>Google<\/title>/)
end
-- snip --
featureのGiven、Whenなどに記述された内容に関して、正規表現で一致したコードが実行される仕組みです。 注意点として、正規表現でマッチさせるため、スペースなどが厳密に一致している必要があります。
一致していない場合は、以下の様にステップが存在しない旨のメッセージが表示され、pending(実行されない処理)が記述されたステップの雛形が出力されます。 最初にテストシナリオを書く際、まずfeatureを記述して実行し、雛形を使ってステップを書き始めるのが良いでしょう。
$ bundle exec cucumber features/user/user_pc_to_internet_host/web.feature
Feature: Google 検索
開発者として、
Google で検索したい
なぜなら開発するときによく調べものをするから
Scenario: Web ブラウザで Google を開く # features/user/user_pc_to_internet_host/web.feature:7
Given 社内 PC # features/user/user_pc_to_internet_host/web.feature:8
And インターネット上のサーバ # features/user/user_pc_to_internet_host/web.feature:9
When 社内 PC にログイン # features/user/user_pc_to_internet_host/web.feature:10
And ブラウザでインターネット上のサーバの Google のページを開く # features/user/user_pc_to_internet_host/web.feature:11
Then Google のトップページが表示 # features/user/user_pc_to_internet_host/web.feature:12
1 scenario (1 undefined)
5 steps (5 undefined)
0m0.021s
You can implement step definitions for undefined steps with these snippets:
Given("社内 PC") do
pending # Write code here that turns the phrase above into concrete actions
end
Given("インターネット上のサーバ") do
pending # Write code here that turns the phrase above into concrete actions
end
When("社内 PC にログイン") do
pending # Write code here that turns the phrase above into concrete actions
end
When("ブラウザでインターネット上のサーバの Google のページを開く") do
pending # Write code here that turns the phrase above into concrete actions
end
Then("Google のトップページが表示") do
pending # Write code here that turns the phrase above into concrete actions
end
ステップの中ではRubyあるいはそこから連携して実行できる任意の処理が記述できるため、リンクのアップダウンやsyslogの確認といったことも容易に実施できます。
これをシナリオに従い順番に実行していくことで、期待どおりの動作になるかも含め、プログラムで確認します。
それぞれの単語の意味は以下のとおりです。それぞれ And
で複数記述できます。
単語 | 意味 |
---|---|
Given | テストの前提条件を記述。サーバが存在する、サービスが起動している、など。 |
When | アクションを記述。サーバにログインする、pingを実行する、など。 |
Then | アクションの結果を記述。結果が成功である、など。 |
このテストシナリオでは、テストノードの内容を factories.rb
に定義し、Givenで記述できるテストノードの準備処理を virtual_host.rb
に記述しています。
記述したシナリオファイルを実行するには、以下のコマンドを実行します。
$ bundle exec cucumber features/user/user_pc_to_internet_host/web.feature
Feature: Google 検索
開発者として、
Google で検索したい
なぜなら開発するときによく調べものをするから
Scenario: Web ブラウザで Google を開く # features/user/user_pc_to_internet_host/web.feature:7
Given 社内 PC # features/step_definitions/virtual_host.rb:2
And インターネット上のサーバ # features/step_definitions/virtual_host.rb:42
When 社内 PC にログイン # features/step_definitions/client_steps.rb:2
And ブラウザでインターネット上のサーバの Google のページを開く # features/step_definitions/google_steps.rb:2
Then Google のトップページが表示 # features/step_definitions/success_steps.rb:24
1 scenario (1 passed)
5 steps (5 passed)
0m16.552s
シナリオファイルを全てするには、以下のコマンドを実行します。現在の構成では、シナリオは直列に実行されます。
$ bundle exec rake
/usr/bin/ruby2.3 -S bundle exec cucumber --tags ~@wip
Feature: DMZ から インターネット上のサーバへの ping
ネットワーク管理者として、
DMZ からインターネット上のサーバにつながるか確認したい
なぜなら DMZ のサーバはインターネット上のサーバにアクセスする必要があるから
Scenario: DMZ からインターネット上のサーバへの ping # features/admin/dmz_host_to_internet_host/ping.feature:7
Given DMZ のサーバ # features/step_definitions/virtual_host.rb:18
And インターネット上のサーバ # features/step_definitions/virtual_host.rb:42
When DMZ のサーバにログイン # features/step_definitions/client_steps.rb:6
And インターネット上のサーバに ping # features/step_definitions/ping_steps.rb:44
Then ping 成功 # features/step_definitions/success_steps.rb:19
-- snip --
Feature: 社内テスト環境サーバ設定
開発者として、
社内テスト環境サーバに telnet でログインしたい
なぜならテスト環境を設定するから
Scenario: 社内テスト環境サーバへ telnet でログイン # features/user/user_pc_to_test_host/telnet.feature:7
Given 社内 PC # features/step_definitions/virtual_host.rb:2
And 社内のテスト環境サーバ # features/step_definitions/virtual_host.rb:10
When 社内 PC にログイン # features/step_definitions/client_steps.rb:2
And 社内のテスト環境サーバに telnet でログイン # features/step_definitions/telnet_steps.rb:2
Then ログイン成功 # features/step_definitions/success_steps.rb:6
32 scenarios (32 passed)
158 steps (158 passed)
6m39.509s
既存の1つシナリオを例にとって、どのように動作するかを追ってみましょう。対象は、先程実行した features/user/user_pc_to_internet_host/web.feature です。
cucumberではfeatureの実行時に、実行されたstepのファイルと行番号が表示されますので、それを順に見てみます。
$ bundle exec cucumber features/user/user_pc_to_internet_host/web.feature
Feature: Google 検索
開発者として、
Google で検索したい
なぜなら開発するときによく調べものをするから
Scenario: Web ブラウザで Google を開く # features/user/user_pc_to_internet_host/web.feature:7
Given 社内 PC # features/step_definitions/virtual_host.rb:2
And インターネット上のサーバ # features/step_definitions/virtual_host.rb:42
When 社内 PC にログイン # features/step_definitions/client_steps.rb:2
And ブラウザでインターネット上のサーバの Google のページを開く # features/step_definitions/google_steps.rb:2
Then Google のトップページが表示 # features/step_definitions/success_steps.rb:24
1 scenario (1 passed)
5 steps (5 passed)
0m16.552s
まず、「Scenario: Web ブラウザで Google を開く」のシナリオで、Given 社内 PCが実行されています。
Given(/^社内 PC$/) do
@user_pc = TestNode.new(attributes_for(:user_pc))
end
ここでは、NetTesterに仮想テストノード user_pc
の作成を依頼し、@user_pc
という変数に格納しています。
この値は、この後のステップで利用します。
user_pc
がどのような定義の仮想テストノードであるかは、features/factories.rb:32に記述されています。
factory :user_pc, class: TestNode do
name 'user_pc'
internal_network_host
ip_address '10.10.10.4'
physical_port_number 2
vlan_id 2025
mac_address '00:00:5E:00:53:03'
end
IPアドレスやMACアドレス、VLANなどの値の他に、このファイルでは、この仮想テストノードはどの拠点に作成されるかも定義しています。
user_pc
は internal_ketwork_host
であるという定義を内包しています。同じファイル内に、
trait :internal_network_host do
tester_set_name 'yoyodyne'
netmask '255.255.255.0'
gateway '10.10.10.254'
virtual_port_number
end
という記述があり、 tester_set_name
が拠点を示す値です。これにより user_pc
は拠点 yoyodyne
に作成されることになります。
拠点 yoyodyne
の示すIPアドレスは features/support/tester_sets.rb に定義されており、このIPアドレスで起動しているNetTesterに仮想ノードの作成を依頼し、physical_port_number
とvirtual_port_number
の値を使ってネットワークへの繋ぎ込みも行ってもらいます。
def tester_sets
{
'yoyodyne' => {
ip_address: '172.16.0.2',
},
'tajimax' => {
ip_address: '172.16.0.3',
}
}
end
次に、And インターネット上のサーバが実行されています。
先程の user_pc
同様、 internet_host
を作成して繋ぎこみ、@internet_host
に格納しています。この値は後のステップで利用します。
Given(/^インターネット上のサーバ$/) do
@internet_host = TestNode.new(attributes_for(:internet_host))
end
ここまでで、環境を揃えるGivenは終了です。
Givenの後は、実際に何かが実施されるWhenを実行します。
Whenでは、When 社内 PC にログインが実行されています。
When(/^社内 PC にログイン$/) do
@src_host = @user_pc
end
ここでは単に、@src_host
という変数に、 @user_pc
を代入しているだけです。この変数は後で使います。しかし、なぜこのような記述になっているのでしょうか。
沢山のシナリオを記述していくと、同じようなステップの使い回しがうまくできなくなって何度も同じ事を記述するといった事が頻繁に発生します。 今回の記述は冗長に見えますが、何度も作りなおし、日本語を読んで意味がわかりやすくなるように、かつ、なるべく使い回しが効くように整理した結果、このようになっています。
次のWhenでは、And ブラウザでインターネット上のサーバの Google のページを開くが実行されています。
この内容が実質、実際に通信をする部分の要になっています。
When(/^ブラウザでインターネット上のサーバの Google のページを開く$/) do
cd('.') do
@internet_host.exec("rm -f /tmp/index.html; echo '<title>Google</title>' | tee /tmp/index.html", sync: true)
@https_service = @internet_host
@https_service.exec("ruby -rwebrick -rwebrick/https -e 'WEBrick::HTTPServer.new(:DocumentRoot => \"/tmp\", :Port => 443, :SSLEnable => true, :SSLCertName => [[\"CN\", WEBrick::Utils::getservername]] ).start'")
@src_host.exec("sudo mkdir -p /etc/netns/#{@src_host.name}", sync: true)
@src_host.exec("echo '198.51.100.3 www.google.com' | sudo tee /etc/netns/#{@src_host.name}/hosts >/dev/null", sync: true)
@process_id = @src_host.exec('curl -L --insecure https://www.google.com/ | iconv -f SHIFT-JIS -t UTF8', delayed: true)
end
end
今までのステップで設定した変数が全て登場し、実際にやりとりを行うのがこのステップです。
内容としては、 @internet_host
上で実際にWebサーバを起動し、@src_host
から curl
でアクセスする、という処理になります。サーバ側では、うまくアクセスできた時に「<title>Google</title>」という文字列を含んだWebページを返却します。
NetTesterの仮想ノードの実体はnetnsであるため、その中で各プロセスを起動させます。 hostsにホスト名とIPアドレスの対応を書き込んだりと、様々な騙しのテクニックを活用して擬似的なインターネットを実現します。
ここまでがWhenの処理です。
最後にThenの処理を実行します。
Thenでは、Then Google のトップページが表示が実行されています。
この中では、結果の値を取得し、期待どおりの値となっているかを確認しています。
Then(/^Google のトップページが表示$/) do
result = @src_host.result(@process_id)
expect(result).to match(/<title>Google<\/title>/)
end
今回のWhenではうまくWebサーバにアクセスできた場合に「<title>Google</title>」の文字列が入ったWebページを返却する処理になっていたため、その値が含まれているかどうかを確認することで、処理が成功したかどうかを確認しています。
これでシナリオ実行の流れは終了です。引き続き、ハンズオンの課題にチャレンジしてみましょう。