ビヘイビア駆動とかよく分からないけど、とりあえずRSpec書きながらプロシージャを作ってみる #2
指定されたmessage_codeが見つからない場合にエラーメッセージを出してみる
前回のソースにエラー処理も足そうよ、という訳で以下のようにスペックを書きます。
it "(3) 存在しないmessage_codeを指定するとエラーメッセージとエラーコードが返る" do cursor = conn.parse(target_plsql) cursor.bind_param(':iv_message_code', 'MES_ERROR_TEST') cursor.bind_param(':iv_token1', '') cursor.bind_param(':iv_token2', '') cursor.bind_param(':iv_token3', '') cursor.bind_param(':iv_token4', '') cursor.bind_param(':iv_token5', '') cursor.bind_param(':ov_message', nil, String, 2000) cursor.bind_param(':ov_retcode', nil, String, 10) cursor.exec cursor[':ov_message'].should == "GET_MESSAGE ERROR:存在しないエラーコードが指定されました。" cursor[':ov_retcode'].should == "1" end
ついカッとなって引数を増やしてしまったので、(2)のケースも修正する必要がありやんす。。
そしてできたのが以下のPL/SQLコード。
CREATE OR REPLACE PROCEDURE GET_MESSAGE( iv_message_code IN Messages.message_code%TYPE, iv_token1 IN Messages.token1%TYPE, iv_token2 IN Messages.token2%TYPE, iv_token3 IN Messages.token3%TYPE, iv_token4 IN Messages.token4%TYPE, iv_token5 IN Messages.token5%TYPE, ov_message OUT VARCHAR2, ov_retcode OUT VARCHAR2) AS cv_program_name CONSTANT VARCHAR2(100) := 'GET_MESSAGE'; lv_ret_message VARCHAR2(2000); lv_error_message VARCHAR2(2000); BEGIN SELECT message INTO lv_ret_message FROM Messages WHERE message_code = iv_message_code; -- OUT変数にMessagesから取得した文字列を代入 ov_message := lv_ret_message; -- 正常終了 ov_retcode := '0'; EXCEPTION WHEN NO_DATA_FOUND THEN lv_error_message := cv_program_name || ' ERROR:存在しないエラーコードが指定されました。'; ov_message := lv_error_message; ov_retcode := '1'; END GET_MESSAGE;
pasta:get_message mahm$ spec -c -fs get_message_spec.rb トークンを指定せずにメッセージを取得するとき - (1) SELECT message FROM Messages WHERE message_code = 'MES000000'で「正常終了しました。」というメッセージを取得できる - (2) 「MES000000」を指定すると「正常終了しました。」というメッセージを取得 - (3) 存在しないmessage_codeを指定するとエラーメッセージとエラーコードが返る Finished in 0.158438 seconds 3 examples, 0 failures
テストも通りました、と。
そろそろ本題の「私の名前は もっこす です。」を実装する
そろそろ本題の機能(もっこす)を実装してみましょう。例のごとく先にスペックを記述します。前回までの機能とは違うので新しくdescribeを記述しますよ。
describe "トークンを1つ指定してメッセージを取得するとき" do before(:all) do # テスト前準備 apxe/apxe@xeでデータベースへ接続 conn = OCI8.new('apxe','apxe','xe') end after(:all) do # データベースからログオフ conn.logoff end it "(1)「MES000001」でトークン1に「もっこす」と指定すると「私の名前は もっこす です。」と返る" do cursor = conn.parse(target_plsql) cursor.bind_param(':iv_message_code', 'MES000001') cursor.bind_param(':iv_token1', 'もっこす') cursor.bind_param(':iv_token2', '') cursor.bind_param(':iv_token3', '') cursor.bind_param(':iv_token4', '') cursor.bind_param(':iv_token5', '') cursor.bind_param(':ov_message', nil, String, 2000) cursor.bind_param(':ov_retcode', nil, String, 10) cursor.exec cursor[':ov_message'].should == "私の名前は もっこす です。" cursor[':ov_retcode'].should == "0" end end
とりあえずエラーを出してみる。
トークンを1つ指定してメッセージを取得するとき - (1)「MES000001」でトークン1に「もっこす」と指定すると「私の名前は もっこす です。」と返る (FAILED - 1) 1) 'トークンを1つ指定してメッセージを取得するとき (1)「MES000001」でトークン1に「もっこす」と指定すると「私の名前は もっこす です。」と返る' FAILED expected: "私の名前は もっこす です。", got: "GET_MESSAGE ERROR:存在しないエラーコードが指定されました。" (using ==) ./get_message_spec.rb:105:
前に実装したエラーメッセージが表示されてて良い感じですね。
message_code | message | token1 | token2 | token3 | token4 | token5 |
MES000001 | 私の名前は &NAME です。 | &NAME |
上記のような形でMessagesにデータ登録して実装してみましょう。
INSERT INTO "APXE"."MESSAGES" (ID, MESSAGE_CODE, MESSAGE, TOKEN1) VALUES ('2', 'MES000001', '私の名前は &NAME です。', '&NAME') コミットは成功しました
そしてエラー処理とかは考えずに、指定箇所が置換されるだけのソースを書いてみます。
create or replace PROCEDURE GET_MESSAGE( iv_message_code IN Messages.message_code%TYPE, iv_token1 IN Messages.token1%TYPE, iv_token2 IN Messages.token2%TYPE, iv_token3 IN Messages.token3%TYPE, iv_token4 IN Messages.token4%TYPE, iv_token5 IN Messages.token5%TYPE, ov_message OUT VARCHAR2, ov_retcode OUT VARCHAR2) AS cv_program_name CONSTANT VARCHAR2(100) := 'GET_MESSAGE'; rec_message Messages%ROWTYPE; lv_ret_message VARCHAR2(2000); lv_error_message VARCHAR2(2000); BEGIN SELECT * INTO rec_message FROM Messages WHERE message_code = iv_message_code; lv_ret_message := rec_message.message; -- TOKEN1が指定されている場合に埋め込み処理 IF iv_token1 IS NOT NULL THEN -- レコードに設定されているTOKEN1の値がmessage中に設定されているか? IF REGEXP_LIKE( rec_message.message, rec_message.token1, 'i') = TRUE THEN lv_ret_message := REGEXP_REPLACE( lv_ret_message, rec_message.token1, iv_token1 ); END IF; END IF; -- OUT変数にMessagesから取得した文字列を代入 ov_message := lv_ret_message; -- 正常終了 ov_retcode := '0'; EXCEPTION WHEN NO_DATA_FOUND THEN lv_error_message := cv_program_name || ' ERROR:存在しないエラーコードが指定されました。'; ov_message := lv_error_message; ov_retcode := '1'; END GET_MESSAGE;
コンパイルが通ったのでスペック実行!
pasta:get_message mahm$ spec -c -fs get_message_spec.rb トークンを指定せずにメッセージを取得するとき - (1) SELECT message FROM Messages WHERE message_code = 'MES000000'で「正常終了しました。」というメッセージを取得できる - (2) 「MES000000」を指定すると「正常終了しました。」というメッセージを取得 - (3) 存在しないmessage_codeを指定するとエラーメッセージとエラーコードが返る トークンを1つ指定してメッセージを取得するとき - (1)「MES000001」でトークン1に「もっこす」と指定すると「私の名前は もっこす です。」と返る Finished in 1.247753 seconds 4 examples, 0 failures
軽やかに通りました。
ここまでRSpecを使ってきて思うこと
やはりスペックファイルの書き方の方針に悩みます。。
ノープランで書いてきましたが、どんな単位でdescribeを書いていくべきか、その中のitはどのように区分けするか、この辺は方針として持っておかないと混沌としていきますね。itの書き方の整理の付け方も難しいです。
そもそもクラスの振る舞いを記述するためのものであってPL/SQLのプロシージャの振る舞いを記述する想定はないのですから、ノープランでやれば混沌とするのがむしろ当たり前ではありますが。。
次回はこの混沌としたSpecファイルを少し整理してみようと思います。
続き:ビヘイビア駆動とかよく分からないけど、とりあえずRSpec書きながらプロシージャを作ってみる #3 まとめ - ランバダ
ビヘイビア駆動とかよく分からないけど、とりあえずRSpec書きながらプロシージャを作ってみる #1
とりあえず簡単なプロシージャをRSpec書きながら作ってみようと思います。
どんなの作るん?
PROCEDURE get_message( iv_message_code IN Messages.message_code%TYPE, iv_token1 IN Messages.token1%TYPE, iv_token2 IN Messages.token2%TYPE, iv_token3 IN Messages.token3%TYPE, iv_token4 IN Messages.token4%TYPE, iv_token5 IN Messages.token5%TYPE, ov_message OUT VARCHAR2 );
iv_message_codeにメッセージコードを入力すると、対応したメッセージ文字列が返ってくるようなプロシージャです。テーブルを以下のように設定しておき
message_code | message | token1 | token2 | token3 | token4 | token5 |
MES100000 | 私の名前は &NAME です。 | &NAME |
get_message( iv_message_code => 'MES10000', iv_token1 => 'もっこす', ov_message => lv_message )
てな感じでプロシージャを実行すれば「私の名前は もっこす です。」とlv_messageに突っ込まれるイメージです。簡単すぎてすんません。。
とりあえずRSpec書いてみるか(ノープラン)
# # messagesテーブルに保存されたメッセージを返すPL/SQLプログラムを # RSpec書きながら作ってみるテスト # require 'rubygems' require 'spec' require 'oci8' # # テスト対象のPL/SQLプロシージャ # (こんな感じのプロシージャ作る) # PROCEDURE get_message( iv_message_code IN Messages.message_code%TYPE, # iv_token1 IN Messages.token1%TYPE, # iv_token2 IN Messages.token2%TYPE, # iv_token3 IN Messages.token3%TYPE, # iv_token4 IN Messages.token4%TYPE, # iv_token5 IN Messages.token5%TYPE, # ov_message OUT VARCHAR2 ); # target_plsql = <<-PLSQL BEGIN get_message(:iv_message_code, :iv_token1, :iv_token2, :iv_token3, :iv_token4, :iv_token5, :ov_message); END; PLSQL # # データベース接続用変数 conn = nil # describe "トークンを指定せずにメッセージを取得するとき" do before(:all) do # テスト前準備 apxe/apxe@xeでデータベースへ接続 conn = OCI8.new('apxe','apxe','xe') end after(:all) do # データベースからログオフ conn.logoff end it "(1) SELECT message FROM Messages WHERE message_code = 'MES000000'で「正常終了しました。」というメッセージを取得できる" do cursor = conn.exec("SELECT message FROM Messages WHERE message_code = 'MES000000'") cursor.fetch.should == ["正常終了しました。"] end it "(2) 「MES000000」を指定すると「正常終了しました。」というメッセージを取得" do cursor = conn.parse(target_plsql) cursor.bind_param(':iv_message_code', 'MES000000') cursor.bind_param(':iv_token1', '') cursor.bind_param(':iv_token2', '') cursor.bind_param(':iv_token3', '') cursor.bind_param(':iv_token4', '') cursor.bind_param(':iv_token5', '') cursor.bind_param(':ov_message', nil, String, 2000) cursor.exec cursor[':ov_message'].should == "正常終了しました。" end end
既にデータベースにMessagesというテーブルを作成し、一つメッセージを登録してあるので(1)のテストは成功します。でも、まだプロシージャを作っていないので(2)のテストは下記のようにコケます。
トークンを指定せずにメッセージを取得するとき - (1) SELECT message FROM Messages WHERE message_code = 'MES000000'で「正常終了しました。」というメッセージを取得できる - (2) 「MES000000」を指定すると「正常終了しました。」というメッセージを取得 (ERROR - 1) 1) OCIError in 'トークンを指定せずにメッセージを取得するとき (2) 「MES000000」を指定すると「正常終了しました。」というメッセージを取得' ORA-06550: 行2、列3: PLS-00201: 識別子GET_MESSAGEを宣言してください。 ORA-06550: 行2、列3: PL/SQL: Statement ignored stmt.c:539:in oci8lib.so /Library/Ruby/Site/1.8/oci8.rb:759:in `exec' /Library/Ruby/Site/1.8/oci8.rb:142:in `do_ocicall' /Library/Ruby/Site/1.8/oci8.rb:759:in `exec' ./get_message_spec.rb:60: Finished in 0.134598 seconds 2 examples, 1 failure
(1)のテストはdescribeの文章的には無い方がよさげですが、そもそもデータベースにテストデータ入ってるんかいという確認が欲しかったので付けました。
そういうわけで、(2)のテストを成功させるべくプロシージャを書いてみましょう。
プロシージャを書く
テストを成功させるために最小限のプロシージャを書きます。
CREATE OR REPLACE PROCEDURE GET_MESSAGE( iv_message_code IN Messages.message_code%TYPE, iv_token1 IN Messages.token1%TYPE, iv_token2 IN Messages.token2%TYPE, iv_token3 IN Messages.token3%TYPE, iv_token4 IN Messages.token4%TYPE, iv_token5 IN Messages.token5%TYPE, ov_message OUT VARCHAR2 ) AS lv_ret_message VARCHAR2(2000); BEGIN SELECT message INTO lv_ret_message FROM Messages WHERE message_code = iv_message_code; -- OUT変数にMessagesから取得した文字列を代入 ov_message := lv_ret_message; END GET_MESSAGE;
そんでもってspecを実行!
pasta:get_message mahm$ spec -c -fs get_message_spec.rb トークンを指定せずにメッセージを取得するとき - (1) SELECT message FROM Messages WHERE message_code = 'MES000000'で「正常終了しました。」というメッセージを取得できる - (2) 「MES000000」を指定すると「正常終了しました。」というメッセージを取得 Finished in 0.077543 seconds 2 examples, 0 failures
おおー、通ってる。って、そりゃそうだろ。。
ソースコードのせいでエントリが長くなってきたので、続きは次のエントリで。今度はエラーハンドリングでも実装してみますか。
テストデータを自由に追加削除できないようなテーブルをプログラム中で使わなければならないときのテスト
例えばシステムログが保存されているテーブルなど、プロジェクト内規約的にもライセンス的にもデータ削除しちゃいけないテーブルがあったりします。で、そのテーブルの情報を元にロジックをごにょごにょしなきゃならないときがあって、そういう場合テストデータをどうしようか、と。そんなことを考えていました。(ちなみにPL/SQLの話です)
案1) そっくりなテーブルを作って、テスト中はそっちのテーブルを参照する。
普通の方法なんですが、本番リリース時にプログラム内のテーブル参照箇所をいじらなければいけないのが何ともはやです。
案2)そういうテーブルを参照するところはViewにしておいて、ViewのFROM句をテスト中はテストテーブルを参照するようにしておく
こっちだとソースコードをいじらずに本番リリースできる分、良さげです。ただ、本番環境のViewと開発環境のViewが異なるということで、後々問題を起こしそうな気がします。バージョン管理が弱めなプロジェクトでは半年ぐらい経った後に問題が起こりそうです。
もっと良い方法ないかなぁ
別スキーマに丸ごとコピーを作って……とかは一身上の都合でできません。案2で行こうかなぁ、とか考えているのですが、こういうのっていろいろ良い方法がどこかにありそうだよなぁ、とか思う今日この頃です。
ruby-oci8でPL/SQLを呼んだとき、PL/SQL例外はどう処理される?
簡単なコードで検証してみた。
-- 例外を起こすだけのプロシージャ CREATE OR REPLACE PROCEDURE EXCP_TEST_1 AS test EXCEPTION; BEGIN RAISE test; EXCEPTION WHEN test THEN RAISE; END EXCP_TEST_1;
これをsqlplus上で実行すると以下のようになる。
Connected. SQL> begin 2 apxe.excp_test_1(); 3 end; 4 / begin * ERROR at line 1: ORA-06510: PL/SQL: ユーザー定義の例外が発生しましたが、処理されませんでした ORA-06512: "APXE.EXCP_TEST_1", 行7 ORA-06512: 行2
ふむふむ。ではrubyで実行すると?
pasta:learn_rspec mahm$ irb >> require 'oci8' => true >> OCI8.new('apxe','******','xe').exec('begin apxe.excp_test_1(); end;') OCIError: ORA-06510: PL/SQL: ユーザー定義の例外が発生しましたが、処理されませんでした ORA-06512: "APXE.EXCP_TEST_1", 行7 ORA-06512: 行1 from stmt.c:539:in oci8lib.so from /Library/Ruby/Site/1.8/oci8.rb:759:in `exec' from /Library/Ruby/Site/1.8/oci8.rb:142:in `do_ocicall' from /Library/Ruby/Site/1.8/oci8.rb:759:in `exec' from /Library/Ruby/Site/1.8/oci8.rb:255:in `exec' from (irb):2
OCIErrorが発生するんですね。じゃあちゃんとした例外を返すようにしてみよう。
-- NO_DATA_FOUND例外を起こすプロシージャ CREATE OR REPLACE PROCEDURE EXCP_TEST_2 AS test EXCEPTION; BEGIN RAISE test; EXCEPTION WHEN test THEN RAISE NO_DATA_FOUND; END EXCP_TEST_2;
>> require 'oci8' => true >> OCI8.new('apxe','******','xe').exec('begin apxe.excp_test_2(); end;') OCINoData: No Data from stmt.c:539:in oci8lib.so from /Library/Ruby/Site/1.8/oci8.rb:759:in `exec' from /Library/Ruby/Site/1.8/oci8.rb:142:in `do_ocicall' from /Library/Ruby/Site/1.8/oci8.rb:759:in `exec' from /Library/Ruby/Site/1.8/oci8.rb:255:in `exec' from (irb):3 >>
OCINoDataとな? ということはRuby側で例外処理したい場合はこんな風に書けば良いのかな。
pasta:learn_rspec mahm$ irb >> require 'oci8' => true >> begin ?> OCI8.new('apxe','apxe','xe').exec('begin apxe.excp_test_2(); end;') >> rescue OCINoData >> puts "OCINoData was catched!" >> end OCINoData was catched! => nil
実際にはエラー内容をもっと詳細にロギングしたり、プロジェクト内で定義したエラーコードを返したりしなければならないのでしょう。raise_application_errorを使っていくのも手か。
Oracle PL/SQL Best PracticeのChapter6ではQuest Error Managerを使用する例が載っていたりと、Error Handling周りの話はなかなか興味深い話で一杯です。ちゃんと考えなきゃね。
Oracle PL/SQL Best Practices: Write the Best PL/SQL Code of Your Life
- 作者: Steven Feuerstein
- 出版社/メーカー: O'Reilly Media
- 発売日: 2007/11/01
- メディア: ペーパーバック
- クリック: 10回
- この商品を含むブログ (3件) を見る
RubyからOracleにちょっかいを出すためにruby-oci8をmake install
何故かsudo gem install ruby-oci8では上手く行かなかったので、RubyForgeからファイル一式をダウンロードしてmakeからやることにします。
ruby setup.rb config checking for load library path... DYLD_LIBRARY_PATH... checking /u01/app/oracle/product/10.2.0/client_1/lib... yes /u01/app/oracle/product/10.2.0/client_1/lib/libclntsh.dylib.10.1 looks like a full client. checking for cc... ok checking for gcc... yes checking for LP64... no checking for ruby header... ok Get the version of Oracle from SQL*Plus... 1020 try -I/u01/app/oracle/product/10.2.0/client_1/rdbms/public checking for oci.h... yes checking for OCIInitialize() in oci.h... no Running make for $ORACLE_HOME/rdbms/demo/demo_rdbms.mk (build) ...OK checking for OCIInitialize() in oci.h... no --------------------------------------------------- error messages: cannot compile OCI --------------------------------------------------- See: * http://ruby-oci8.rubyforge.org/ja/HowToInstall.html * http://ruby-oci8.rubyforge.org/ja/ReportInstallProblem.html
しかし初回は上手くいかず。どうやら-arch i386オプションが必要らしい。オプションを加えてやると上手くいった。
pasta:ruby-oci8-1.0.4 mahm$ export ARCHFLAGS="-arch i386" pasta:ruby-oci8-1.0.4 mahm$ make ruby setup.rb config checking for load library path... DYLD_LIBRARY_PATH... checking /u01/app/oracle/product/10.2.0/client_1/lib... yes /u01/app/oracle/product/10.2.0/client_1/lib/libclntsh.dylib.10.1 looks like a full client. checking for cc... ok checking for gcc... yes checking for LP64... no checking for ruby header... ok Get the version of Oracle from SQL*Plus... 1020 try -I/u01/app/oracle/product/10.2.0/client_1/rdbms/public checking for oci.h... yes checking for OCIInitialize() in oci.h... yes checking for OCIEnvCreate()... yes checking for OCITerminate()... yes checking for OCILobOpen()... yes checking for OCILobClose()... yes checking for OCILobCreateTemporary()... yes checking for OCILobGetChunkSize()... yes checking for OCILobLocatorAssign()... yes checking for OCIRowidToChar()... yes creating ../../lib/oci8.rb from /Users/mahm/Downloads/ruby-oci8-1.0.4/ext/oci8/../../lib/oci8.rb.in (以下略)
pasta:ruby-oci8-1.0.4 mahm$ irb >> require 'oci8' => true >> OCI8.new('apxe','******','xe').exec('select SYSDATE from dual'){|out| puts out} 2009/03/06 17:39:13 => 1 >>
これで土台が整ったことに。今から用事があって外出するので、続きはまた後で。。
まずはMacOSXにOracle databse10g Client for IntelMacをインストール
http://www.oracle.com/technology/software/products/database/oracle10g/htdocs/10204macsoft.html
こちらから10g Clientをダウンロードします。(何故か日本語のサイトの方にはIntelMac版がない。。)
FullPackageとInstantどちらでもいいっぽいのですが、私はFullの方をDLしました。で、中に入っているohomeの中身を下記ディレクトリへ格納。
/u01/app/oracle/product/10.2.0/client_1
ユーザーのhomeディレクトリ直下の.bash_profile(なければ作成)に以下のように環境変数を設定。
export ORACLE_BASE=/u01/app/oracle export ORACLE_HOME=$ORACLE_BASE/product/10.2.0/client_1 export DYLD_LIBRARY_PATH=$ORACLE_HOME/lib:$DYLD_LIBRARY_PATH export PATH=$ORACLE_HOME/bin:$PATH export NLS_LANG=japanese_japan.UTF8
source .bash_profileで環境変数を適用します。次にtnsnames.oraのセッティング。下記ディレクトリへ格納します。
/u01/app/oracle/product/10.2.0/client_1/network/admin
# tnsnamens.ora XE = (DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.11.100)(PORT = 1521)) ) (CONNECT_DATA = (SERVICE_NAME = xe) ) )
sqlplusの疎通確認。
pasta:admin mahm$ sqlplus /nolog SQL*Plus: Release 10.2.0.4.0 - Production on 金 3月 6 17:31:48 2009 Copyright (c) 1982, 2007, Oracle. All Rights Reserved. SQL> conn apxe/******@xe Connected. SQL>
Oracle PL/SQLのUnit TestingをRSpecで行う試み
PL/SQLのUnit Testing FrameworkにutPLSQLというものがあるけれども、正直使いやすいとは言えないです。ドキュメントを見ているだけで少しゲンナリする。
PL/SQLプロシージャをCallableStatementで呼んでJUnitでテストするという試みはあるらしいけれども、Javaでやるのも仕事なら良いけど勉強がてらだと何だか重いなぁ、なんて思い、いっそRubyでやってみることにします。
じゃあRubyのTestUnit使おうか?というのも、何だか面白くない。どうせならビヘイビア駆動開発なんつってRSpecで行こう。
そういう訳でRSpecでPL/SQLパッケージをビヘイビア駆動開発してみようと思います。
# 平日のこんな時間に記事を書いていますが、ニートになった訳ではないです。。一応、長期休暇中です。