BDD(振る舞い型駆動開発)ツールであるbehaveを使ってサーバテストはできないものか

BDD(Behavior-driven development)と言う開発スタイルがある。 振る舞いを先に定義して、そのテストコードを書き、実装を行うことで、意図した振る舞いをする実装である正当性を検証できるもの、と言うのが雑な理解。 そのための、と言うわけではないが、BDDツールとしてpythonでは behave と言うものがある。(他にもLettuceとかFreshenとか色々あるようだね) 今回は behave をサーバーの動作確認ツールとして使えないかな、と思ったあたりで、簡単な利用方法を見ていく。 Ruby界隈ではCucumber辺りがその発祥なんだろうか、残念ながら僕はRuby界隈における歴史については詳しくないので、知っている人に聞いてくれ。

BDD(振る舞い型駆動開発)ツールであるbehaveを使ってサーバテストはできないものか

環境

環境については、以下の通り。

1
2
3
4
5
$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.1 LTS"

今回の手順

利用方法を見ていくにあたって、今回は以下の手順に沿って行う。

  1. behaveをインストールする
  2. 振る舞いを記述する
  3. behaveを実行すると、テストの実体を実装するように指示される
  4. テストを実装する
  5. behaveを実行すると、テストに失敗する
  6. 意図した振る舞いをするサーバー設定を行う
  7. behaveを実行すると、テストに成功する
  8. 終わり

長い。

でもその後もちょっと続くぞい。

behaveのインストール

behaveのインストール自体は簡単。

1
2
$ sudo apt install -y python-pip
$ pip install behave

C拡張等も無いので追加権限も必要ない。必要になるのはテストコード次第なので、テストコード実装の段階になってから。

振る舞い features/*.feature を記述する

振る舞いの記述には、 features ファイルを記述する必要があります。

まずは作ってみよう。動作の説明は下記の example1.feature に書いてある通り。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ mkdir /tmp/example1
$ cd /tmp/example1
$ mkdir features
$ mkdir features/steps
$ vi features/example1.feature
Feature: Check server services

  Scenario: Check services
    Given 127.0.0.1でnginxが起動している場合
    When TCPポート80にアクセスしたら
    Then TCPセッションが確立できること

behaveを実行すると、テストの実体を実装するように指示される

では振る舞いを定義したので、behaveを実行してみます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ behave
Feature: Check server services # features/example1.feature:1

  Scenario: Check services         # features/example1.feature:3
    Given 127.0.0.1でnginxが起動している場合 # None
    When TCPポート80にアクセスしたら          # None
    Then TCPセッションが確立できること          # None


Failing scenarios:
features/example1.feature:3  Check services

0 features passed, 1 failed, 0 skipped
0 scenarios passed, 1 failed, 0 skipped
0 steps passed, 0 failed, 0 skipped, 3 undefined
Took 0m0.000s

You can implement step definitions for undefined steps with these snippets:

@given(u'127.0.0.1でnginxが起動している場合')
def step_impl(context):
    raise NotImplementedError(u'STEP: Given 127.0.0.1でnginxが起動している場合')

@when(u'TCPポート80にアクセスしたら')
def step_impl(context):
    raise NotImplementedError(u'STEP: When TCPポート80にアクセスしたら')

@then(u'TCPセッションが確立できること')
def step_impl(context):
    raise NotImplementedError(u'STEP: Then TCPセッションが確立できること')

振る舞いを検証するためのテストコードが足りないと言われるので、これをコピペしてテストコードを実装していこう。

テスト features/steps/*.py を実装する

以下のようにテストを実装してみます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ vi features/steps/example1_test.py
# -*- coding: UTF-8 -*-
from behave import *
import socket

@given(u'127.0.0.1でnginxが起動している場合')
def step_impl(context):
    pass

@when(u'TCPポート80にアクセスしたら')
def step_impl(context):
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(("127.0.0.1", 80))
    context.connection = True

@then(u'TCPセッションが確立できること')
def step_impl(context):
    assert context.connection
  • ちょっと説明端折ってますが context にデータ突っ込んでおくとそれは引き継げます。

behaveを実行すると、テストに失敗する

そしてbehaveを実行すると、テストに失敗します。(成功してしまった君は既にWebサーバが動いているから、ある意味正しい)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ behave 
Feature: Check server services # features/example1.feature:1

  Scenario: Check services         # features/example1.feature:3
    Given 127.0.0.1でnginxが起動している場合 # features/steps/example1_test.py:5 0.001s
    When TCPポート80にアクセスしたら          # features/steps/example1_test.py:9 0.002s
    Traceback (most recent call last):
        File "/tmp/example1/local/lib/python2.7/site-packages/behave/model.py", line 1456, in run
        match.run(runner.context)
        File "/tmp/example1/local/lib/python2.7/site-packages/behave/model.py", line 1903, in run
        self.func(context, *args, **kwargs)
        File "features/steps/example1_test.py", line 12, in step_impl
        client.connect(("127.0.0.1", 80))
        File "/usr/lib/python2.7/socket.py", line 228, in meth
        return getattr(self._sock,name)(*args)
    error: [Errno 111] Connection refused

    Then TCPセッションが確立できること          # None


Failing scenarios:
features/example1.feature:3  Check services

0 features passed, 1 failed, 0 skipped
0 scenarios passed, 1 failed, 0 skipped
1 step passed, 1 failed, 1 skipped, 0 undefined
Took 0m0.003s

ちなみに、途中でテストに失敗すると残りのテストはスキップされます。

意図した振る舞いをするサーバー設定を行う

簡単のため、nginxを入れるだけです。

1
$ sudo apt install -y nginx

behaveを実行すると、テストに成功する

ではもう一度behaveを実行します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ behave 
Feature: Check server services # features/example1.feature:1

  Scenario: Check services         # features/example1.feature:3
    Given 127.0.0.1でnginxが起動している場合 # features/steps/example1_test.py:4 0.000s
    When TCPポート80にアクセスしたら          # features/steps/example1_test.py:7 0.001s
    Then TCPセッションが確立できること          # features/steps/example1_test.py:12 0.000s

1 feature passed, 0 failed, 0 skipped
1 scenario passed, 0 failed, 0 skipped
3 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.001s

小休憩

まずは動くところまで確認できました。
いったん休憩したら、もう少し慣れてみましょう。

もう少し慣れてみる

この辺の記法 http://pythonhosted.org/behave/gherkin.html を見ながら、もう少し慣れてみましょう。

簡単な変数化

シナリオファイル( *.feature )からは、部分的に変数化することができるので、以下のように書くことができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ vi features/steps/example1_test.py
# -*- coding: UTF-8 -*-
from behave import *
import socket

@given(u'{host}でnginxが起動している場合')
def step_impl(context, host):
    context.host = host

@when(u'TCPポート{port:n}にアクセスしたら')
def step_impl(context, port):
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect((context.host, port))
    context.connection = True

@then(u'TCPセッションが確立できること')
def step_impl(context):
    assert context.connection

シナリオファイル( *.feature )には一切手を付けないまま、もう一度実行します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ behave 
Feature: Check server services # features/example1.feature:1

  Scenario: Check services         # features/example1.feature:3
    Given 127.0.0.1でnginxが起動している場合 # features/steps/example1_test.py:4 0.001s
    When TCPポート80にアクセスしたら          # features/steps/example1_test.py:7 0.005s
    Then TCPセッションが確立できること          # features/steps/example1_test.py:12 0.001s

1 feature passed, 0 failed, 0 skipped
1 scenario passed, 0 failed, 0 skipped
3 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.006s

問題なさそうですね。
次に、シナリオファイル( *.feature )に新しいポート番号を追加してみましょう。
コピペするだけだから簡単だね。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ vi features/example1.feature
Feature: Check server services

  Scenario: Check services
    Given 127.0.0.1でnginxが起動している場合
    When TCPポート80にアクセスしたら
    Then TCPセッションが確立できること

  Scenario: Check services
    Given 127.0.0.1でnginxが起動している場合
    When TCPポート22にアクセスしたら
    Then TCPセッションが確立できること

では実行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ behave 
Feature: Check server services # features/example1.feature:1

  Scenario: Check services         # features/example1.feature:3
    Given 127.0.0.1でnginxが起動している場合 # features/steps/example1_test.py:4 0.001s
    When TCPポート80にアクセスしたら          # features/steps/example1_test.py:7 0.010s
    Then TCPセッションが確立できること          # features/steps/example1_test.py:12 0.000s

  Scenario: Check services         # features/example1.feature:8
    Given 127.0.0.1でnginxが起動している場合 # features/steps/example1_test.py:4 0.000s
    When TCPポート22にアクセスしたら          # features/steps/example1_test.py:7 0.004s
    Then TCPセッションが確立できること          # features/steps/example1_test.py:12 0.000s

1 feature passed, 0 failed, 0 skipped
2 scenarios passed, 0 failed, 0 skipped
6 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.017s

後ろで動くスクリプトを柔軟に作っておけば、日本語でテストシナリオを書くだけでテストが増やせます。
プログラム書けない人でも、もとい、プログラムを書かない人たちにも、このシナリオファイルを見せながらなら、どんな試験をしているか説明し易いね。

テーブルを使った表現

でも、数が多くなってきたら冗長じゃない?
そこでテーブル表現ですよ。
今度は、裏で動いているpythonスクリプトはそのままに、シナリオファイル( *.feature )だけを変更して対応してみよう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ vi features/example1.feature
Feature: Check server services

  Scenario Outline: Check services
    Given <host>でnginxが起動している場合
    When TCPポート<port>にアクセスしたら
    Then TCPセッションが確立できること

    Examples: Services
    | host      | port |
    | 127.0.0.1 | 80   |
    | 127.0.0.1 | 22   |

そしてbehaveを実行する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ behave 
Feature: Check server services # features/example1.feature:1

  Scenario Outline: Check services -- @1.1 Services  # features/example1.feature:10
    Given 127.0.0.1でnginxが起動している場合                   # features/steps/example1_test.py:4 0.001s
    When TCPポート80にアクセスしたら                            # features/steps/example1_test.py:7 0.005s
    Then TCPセッションが確立できること                            # features/steps/example1_test.py:12 0.000s

  Scenario Outline: Check services -- @1.2 Services  # features/example1.feature:11
    Given 127.0.0.1でnginxが起動している場合                   # features/steps/example1_test.py:4 0.000s
    When TCPポート22にアクセスしたら                            # features/steps/example1_test.py:7 0.005s
    Then TCPセッションが確立できること                            # features/steps/example1_test.py:12 0.000s

1 feature passed, 0 failed, 0 skipped
2 scenarios passed, 0 failed, 0 skipped
6 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.012s

シナリオファイル( *.feature )もスッキリ見やすくなった。
書き方の工夫は色々ありそうだけど、まずはこんなところ。

おしまい

behaveがどういうものが大体分かりましたね。
きっとこういったものはプログラムを書く人の興味範疇なのかなぁ、と思うのだけど、今回動かしてみた感じでは、例えば Infrataster のような立ち位置に近い気がしている。
個人的にはInfratasterよりbehaveの方が性に合う感じ。
テストコードを自分で書かなきゃいけないのは一緒なので、上手く誰かが作ったものを使いまわせる世界になると良いんだよね。
運用的にはnagiosプラグインとのラッパーなんかと組み合わせて、上手くログを集められたりすると抽象化された記述はこれ1本で行けそうなんだけど、やっぱりそう上手くいかないよねぇ。

と言う感じでしたとさ。おわり。

Built with Hugo
テーマ StackJimmy によって設計されています。