PostgreSQLでjenkinsとpgTAPを使ってCI¶
pgTAP というPostgreSQLでのUnit Testを実行するた めのツールがあります。(ちなみに MySQL用に MyTAP というのもあります。同じ作者です)
インストール¶
% pgxn install pgTAP
なんですが、どうもfreebsdだとうまくgmakeにしてくれなかったので makeをgmakeに書き換えて実行したりしました。(ちなみにpgxnclientは defaultをgmakeにしてmakeへとfall backさせるか、--makeオプションを追加 して明示的に指定するようにするようです)
その後は拡張を有効化。
% psql test -c "CREATE EXTENSION pgtap;"
9.1だとこれで完了です。
続いてDBにインストール。
% psql -d test -f /path/to/pgsql/share/contrib/pgtap.sql
これで終了です。
これだけでもいいのですが、pg_proveというperlツールを入れるとすごく簡単 になるのでpg_prooveも入れましょう。
pg_proveを入れるには、
% cpan TAP::Parser::SourceHandler::pgTAP
とします。
pgTAPの使い方¶
pgTAPの詳しい使い方は ドキュメント を見ていただくとして、まずは概 要を説明します。
なお、 pgTAP best practices という PGCon 2009で発表されたスライドがあり、これにかなり詳しく載っていますの で参照してください。
pgTAPではこんな感じのsqlを書きます。
-- トランザクションとテストプランの開始
BEGIN;
SELECT plan(1);
-- テストの実行
SELECT pass( 'My test passed, w00t!' );
-- テストの終了と掃除
SELECT * FROM finish();
ROLLBACK;
plan()とfinish()については後ほど説明します。BEGINとROLLBACKで囲んでおく のもポイントですね。
直接psqlで実行しても良いですが、pg_proveを使うと便利です。 拡張子を.sqlではなく.pgにしておくと、pg_proveが見つけてくれます。
% pg_prove -d test test.pg (-d はDBを指定するオプション)
test.pg .. ok
All tests successful.
Files=1, Tests=1, 0 wallclock secs ( 0.02 usr + 0.00 sys = 0.02 CPU)
Result: PASS
plan()とfinish()¶
plan()は失敗してsqlの実行が終了してしまうのを防ぐために最初に宣言します。 引数はテストの数です。
SELECT plan( 42 );
-- あるいは
SELECT plan( COUNT(*) ) FROM foo;
一応 SELECT * FROM no_plan(); とすればplanを宣言する必要はないのですが、 planは宣言しておきましょう。
finish()はpgTAPにテストが終了したことを伝えます。
SELECT * FROM finish();
テストとして使える関数¶
ざっくり書き出してみましたが、これ以外にかなり大量の関数があります。
また、全ての関数は:descriptionとして説明を書けるようになっていますが、 省略しても構いません。
- ok(:boolean, :description)
boolean
- is( :have, :want, :description)
:haveと:wantの値の比較
- isnt(:have, :want, :description)
is()の否定
- cmp_ok(:have, :op, :want, :description)
:opで指定した演算子で:haveと:wantを比較
- matches(:have, :regex, :description)
正規表現比較
- imatches( :have, :regex, :description)
大文字小文字無視の正規表現比較
- isa_ok(:have, :regtype, :name)
:haveで指定した値の型を:regtypeと比較
- result_eq(:sql, :sql, :description)
SQLの結果を比較
- set_eq(:sql, :array, :description )
:sqlの結果と:arrayの結果を比較(順序と重複無視)
- set_has(:sql1, :sql2, :description)
:sqlの結果が:sql2に含まれているか
- bag_eq(:sql, :array, :description)
:sqlの結果と:arrayの結果を比較(順序無視、重複は気にする)
- schemas_are(:schemas, :description)
:schemas(配列)で指定したschemaがあるか
- tables_are(:tables, :description)
:tables(配列)で指定したテーブルがあるか
- columns_are(:table, :columns, :description)
:tableに:columns(配列)で指定したカラムがあるか
実際に書いてみる¶
ここからは実際に書いてみます。とはいえ、schemaなどにもよるので、雰囲気 だけとおもってください。
boolean¶
ok()を使います。関数のテストなどに使えると思います。
BEGIN;
SELECT plan(5);
SELECT ok( 9 ^ 2 = 81, 'simple exponential' );
SELECT ok( 9 < 10, 'simple comparison' );
SELECT ok( 'foo' ~ '^f', 'simple regex' );
SELECT ok( 'foo' ~ '^f', 'simple regex' );
SELECT ok( somefunc(10) = 5, 'some func' );
SELECT * from finish();
ROLLBACK;
% pg_prove -v -d try fail.sql
fail.sql .. 1..2 ok 1 not ok 2 # Failed
test 2 # Looks like you failed 1 test of 2 Failed 1/2 subtests
schemaチェック¶
has_tableやtabels_areなどを使います。
BEGIN;
SELECT plan(14);
--
SELECT schemas_are( ARRAY[ 'public', 'contrib', 'biz' ] );
SELECT tables_are( 'biz', ARRAY[ 'users', 'widgets' ] );
--
SELECT has_table( 'users' );
SELECT has_column('users', 'user_id' );
SELECT col_type_is( 'users', 'user_id','integer' );
SELECT col_is_pk( 'users', 'user_id' );
SELECT col_default_is('users', 'state', 'active' );
SELECT has_schema( 'biz' );
SELECT has_type( 'biz', 'timezone' );
SELECT has_index( 'biz', 'users', 'idx_nick' );
SELECT has_function( 'biz', 'get_user_data' );
--
SELECT has_role( 'postgres' );
SELECT has_user('postgres' );
SELECT has_group( 'postgres' );
SELECT * FROM finish();
ROLLBACK;
SQLによるテスト¶
results_eq()を使ってqueryのテストが出来ます。一つ目の引数のSQL結果と二 つ目の引数のSQLの結果が同一かどうかをチェックします。
SELECT results_eq(
'SELECT * FROM active_users()',
'SELECT * FROM users WHERE active ORDER BY nick',
'active_users() should return active users'
);
SELECT results_eq(
'SELECT * FROM active_users()',
$$VALUES ('anna', 'yddad', 'Anna Wheeler', true),
('strongrrl', 'design', 'Julie Wheeler', true),
('theory', 's3kr1t', 'David Wheeler', true)
$$,
'active_users() should return active users' );
SQLを直接書くだけでなく、PREPAREも使えます。
PREPARE users_test AS
SELECT * FROM active_users();
PREPARE users_expect AS
SELECT * FROM users WHERE active ORDER BY nick;
--
SELECT results_eq(
'users_test',
'users_expect',
'We should have users'
);
値をテーブルに入れて反復テスト¶
テストではコーナーケースをテストすることが重要です。postgresはRDBですの で、値を保持するのは得意です。というわけで、テストで使う値をテーブルに 入れておいて、それを使えば楽ですね。
まず値を入れるテーブル(vsets)を定義します。ついでに今回使う関数も定義し ておきます。
-- テスト用の関数定義
CREATE OR REPLACE FUNCTION array_sum(int[]) RETURNS int AS $$
SELECT sum(x)::int FROM unnest($1)x;
$$ LANGUAGE SQL;
CREATE OR REPLACE FUNCTION array_min(int[]) RETURNS int AS $$
SELECT min(x) FROM unnest($1)x;
$$ LANGUAGE SQL;
CREATE OR REPLACE FUNCTION array_max(int[]) RETURNS int AS $$
SELECT max(x) FROM unnest($1)x;
$$ LANGUAGE SQL;
DROP TABLE IF EXISTS vsets ;
CREATE TABLE vsets (
args int[],
min int,
max int,
sum int
);
INSERT INTO vsets VALUES
('{1, 2, 3}', 1, 3, 6),
('{200, 2, 30}', 2, 200, 232)
;
続けて、テストを記述します。
BEGIN;
-- 3とは記載されているテストの数
SELECT plan(COUNT(*)::int * 3) FROM vsets;
SELECT ok(array_min(args) = min) FROM vsets;
SELECT ok(array_max(args) = max) FROM vsets;
SELECT ok(array_sum(args) = sum) FROM vsets;
SELECT * from finish();
ROLLBACK;
こうしておけば、テストケースを追加するには単にvsetsテーブルに値を INSERTするだけです。
INSERT INTO vsets VALUES
('{10000, 20, 444}', 20, 10000, 10464);
制御¶
- skip()
テストを飛ばす
- todo()
指定した数の続くテストをTODOとして飛ばす
pg_prove¶
pg_proveはいろいろな使い方ができます。
ワイルドカード指定したり
% pg_prove tests/*.pg
再帰的にディレクトリをたどったり
% pg_prove -r tests/
並列にjobを走らせたり
% pg_prove -j 4 tests/
STDOUTとSTDERRを両方共STDOUTに出すようにしたり(通常は例外がおきるとSTDERRに出されます)
% pg_prove --merge tests/
tar.gzの中にあるテストを直接実行したり
% pg_prove -a test.tar.gz
できます。便利。
Jenkinsと組み合わせる¶
pg_proveは中身はperlスクリプトで、TAP::Harnessを使用することでさまざま な種類の出力に対応できます。
特にTAP::Formatter::JUnitを使うとjenkinsとすぐに組み合わせることが出来 ます。
以下のように「シェルの実行」を設定します。
pg_prove \
-h localhost -p 5432 -U postgres \
-b /usr/local/pgsql/bin/psql \
--formatter TAP::Formatter::JUnit \
-d test \
-r /home/rudi/test.pg > $WORKSPACE/test-reports/pgTAP.xml
こうしておいて、「JUnitのテスト結果の集計」を
test-reports/pgTAP.xml
としてあげればOKです。
ちなみに、ubuntuのapt-getで入るpg_proveは3.25なんですが、これの終了コー ドの扱いが変なので、以下のようにする必要がありました。
--- /usr/bin/pg_prove.orig 2012-07-30 17:51:41.449543479 +0900
+++ /usr/bin/pg_prove 2012-07-30 17:51:50.125543401 +0900
@@ -87,7 +87,7 @@
} keys %{ $opts->{set} })
);
-exit $app->run ? 0 : 1;
+exit ($app->run ? 0 : 1);
PGPROVE: {
なお、テストの直前に -f pgtap.sql でDBにインストールし、 Post build task な どを使ってテスト後に uninstall するとDBをクリーンに保てて良いかと思いま す。
まとめ¶
テスト大事。もちろんDBでもテストするよね。
pgTAPで使える関数の数すごい。いろいろテストできるね。
pg_proveを使えばjenkinsとの組み合わせも簡単だよ
今回は関数の説明はかなり端折ってるので、ぜひご自分でドキュメントをご覧 下さい。もし要望が多ければ翻訳します。
Comments
comments powered by Disqus