このエントリーをはてなブックマークに追加

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との組み合わせも簡単だよ

今回は関数の説明はかなり端折ってるので、ぜひご自分でドキュメントをご覧下さい。もし要望が多ければ翻訳します。