BDD(振る舞い型駆動開発)ツールであるbehaveを使ってサーバテストはできないものか
Posted on 2016/11/26(Sat) 20:05 in technical
はじめに
BDD(Behavior-driven development)と言う開発スタイルがある。
振る舞いを先に定義して、そのテストコードを書き、実装を行うことで、意図した振る舞いをする実装である正当性を検証できるもの、と言うのが雑な理解。
そのための、と言うわけではないが、BDDツールとしてpythonでは behave と言うものがある。(他にもLettuceとかFreshenとか色々あるようだね)
behave: http://pythonhosted.org/behave/
今回は behave をサーバーの動作確認ツールとして使えないかな、と思ったあたりで、簡単な利用方法を見ていく。
Ruby界隈ではCucumber辺りがその発祥なんだろうか、残念ながら僕はRuby界隈における歴史については詳しくないので、知っている人に聞いてくれ。
環境
環境については、以下の通り。
$ cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=16.04 DISTRIB_CODENAME=xenial DISTRIB_DESCRIPTION="Ubuntu 16.04.1 LTS"
今回の手順
利用方法を見ていくにあたって、今回は以下の手順に沿って行う。
- behaveをインストールする
- 振る舞いを記述する
- behaveを実行すると、テストの実体を実装するように指示される
- テストを実装する
- behaveを実行すると、テストに失敗する
- 意図した振る舞いをするサーバー設定を行う
- behaveを実行すると、テストに成功する
- 終わり
長い。
でもその後もちょっと続くぞい。
behaveのインストール
behaveのインストール自体は簡単。
$ sudo apt install -y python-pip $ pip install behave
C拡張等も無いので追加権限も必要ない。必要になるのはテストコード次第なので、テストコード実装の段階になってから。
振る舞い(features/*.feature)を記述する
振る舞いの記述には、 features ファイルを記述する必要があります。
まずは作ってみよう。動作の説明は下記の example1.feature に書いてある通り。
$ 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を実行してみます。
$ 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)を実装する
以下のようにテストを実装してみます。
$ 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サーバが動いているから、ある意味正しい)
$ 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を入れるだけです。
$ sudo apt install -y nginx
behaveを実行すると、テストに成功する
ではもう一度behaveを実行します。
$ 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)からは、部分的に変数化することができるので、以下のように書くことができます。
$ 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
通報
文字列であれば {host} のように変数名だけ、数値であれば {port:n} のように型を指定します。
詳しくはこちら: http://pythonhosted.org/behave/parse_builtin_types.html
シナリオファイル(*.feature)には一切手を付けないまま、もう一度実行します。
$ 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)に新しいポート番号を追加してみましょう。
コピペするだけだから簡単だね。
$ 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セッションが確立できること
では実行。
$ 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)だけを変更して対応してみよう。
$ 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を実行する。
$ 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本で行けそうなんだけど、やっぱりそう上手くいかないよねぇ。
と言う感じでしたとさ。
おわり。