Skip to content

シナリオ実行の流れ

127.0.0.1 edited this page Feb 13, 2018 · 20 revisions

シナリオ実行の仕組み

このハンズオンでは、ソフトウェアの受け入れテストツールであるcucumberからNetTesterを連携する形で呼び出し、 ネットワークの構成やテストノードの繋ぎこみをテストに合わせて変更しながら、テストノードからパケットを送信するといった処理を行います。

NetTester

NetTesterはRubyからライブラリとして、あるいはShellなどからコマンドとして利用できる、 結線制御や仮想ホスト制御を含んだツールです。

NetTesterはテストフレームワーク自体は含んでいません。 RubyやShellから呼ぶ、あるいはRESTで呼ぶことができるのため、既存テストフレームワークと連携可能である、という表現が正確です。

既存テストフレームワークとしては、 cucumber などがあります。

cucumber

Ruby製の受入テストフレームワークで、試験仕様を日本語で記述できる特徴があります。

日本語でテストを記述する、というのはイメージしにくいかもしませんが、 特定の日本語の文字列に対応するRubyコード(step)を定義しておいて、テストケース(feature)に書かれた日本語に引っかかったstepを実行する、 という仕組みで実現されています。

cucumber テストシナリオのディレクトリ構成

今回のハンズオン環境のテストシナリオのディレクトリ、ファイルは以下のような構成になっています。

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ファイル

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ファイルの中身

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 と virtual_host.rb

このテストシナリオでは、テストノードの内容を factories.rb に定義し、Givenで記述できるテストノードの準備処理を virtual_host.rb に記述しています。

cucumberの実行

記述したシナリオファイルを実行するには、以下のコマンドを実行します。

$ 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

cucumberの実行 (全シナリオ)

シナリオファイルを全てするには、以下のコマンドを実行します。現在の構成では、シナリオは直列に実行されます。

$ 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

「Given 社内 PC」

まず、「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_pcinternal_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_numbervirtual_port_numberの値を使ってネットワークへの繋ぎ込みも行ってもらいます。

def tester_sets
  {
    'yoyodyne' => {
      ip_address: '172.16.0.2',
    },
    'tajimax' => {
      ip_address: '172.16.0.3',
    }

  }
end

「And インターネット上のサーバ」

次に、And インターネット上のサーバが実行されています。

先程の user_pc 同様、 internet_host を作成して繋ぎこみ、@internet_host に格納しています。この値は後のステップで利用します。

Given(/^インターネット上のサーバ$/) do
  @internet_host = TestNode.new(attributes_for(:internet_host))
end

ここまでで、環境を揃えるGivenは終了です。

「When 社内 PC にログイン」

Givenの後は、実際に何かが実施されるWhenを実行します。

Whenでは、When 社内 PC にログインが実行されています。

When(/^社内 PC にログイン$/) do
  @src_host = @user_pc
end

ここでは単に、@src_host という変数に、 @user_pc を代入しているだけです。この変数は後で使います。しかし、なぜこのような記述になっているのでしょうか。

沢山のシナリオを記述していくと、同じようなステップの使い回しがうまくできなくなって何度も同じ事を記述するといった事が頻繁に発生します。 今回の記述は冗長に見えますが、何度も作りなおし、日本語を読んで意味がわかりやすくなるように、かつ、なるべく使い回しが効くように整理した結果、このようになっています。

「And ブラウザでインターネット上のサーバの Google のページを開く」

次の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 Google のトップページが表示」

最後に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ページを返却する処理になっていたため、その値が含まれているかどうかを確認することで、処理が成功したかどうかを確認しています。

これでシナリオ実行の流れは終了です。引き続き、ハンズオンの課題にチャレンジしてみましょう。