Javaデータベースコードの単体テストにHSQLDB + DBUnitを使用する

まえがき



単体テストでは、実際のオブジェクト(つまり、データベース接続、ネットワークソケット、および同様のリソース)を使用しないでください。 これに基づいて多くのホリバーが開発されています-データベースで動作するコードをテストする必要があるか、それが悪い調子です。 テストする場合、これは単体テストと呼ばれるか、機能テスト(または2つのソフトウェア環境/モジュールの共同作業をテストしているため統合テスト)と呼ばれます。 紛争と戦いは終わりません。 読者に聖戦に気を取られるのではなく、この資料を思考の糧として受け入れるようお願いします。 説明したツールは単なるツールであり、その適用可能性はタスクによって決まることを忘れないでください。



ツール選択



おそらく、ユニットテストで最も難しいのは、データベース接続で機能するコードを確認することです(概して、外部オブジェクトで機能するコードを確認することです)。 はい、接続の代わりにモックを使用できますが、JDBCプロバイダーで複数の操作がある場合、後者を使用するコードでモックオブジェクトをキャッチするよりも、モックオブジェクトでミスをする可能性が高くなります。 何が残っていますか? また、データベースサーバーをリポジトリに配置できないため、実際のデータベースを使用するのも良くありません。 この問題の解決策はHSQLDBです。



HSQLDBは​​、完全にJavaで記述されたリレーショナルデータベースです。 同時に、これは非常に注目に値することですが、データベースサーバーを別のインスタンスとして上げることも、Javaアプリケーション内で作成することもできます。 サイズが小さく、データベース全体をメモリに完全に格納する機能(デフォルト)により、HSQLDBは​​単体テストに理想的なデータベースサーバーになります。 JDBCおよびORMの観点からすると、DBMSの実装は重要ではありません(SQL標準に準拠し、DBMSエンジンの拡張機能を乱用しない場合)。ユニットテスト中に、PostgreSQLまたはOracleへの接続をHSQLDBへの接続に簡単に置き換えることができます。



さて、完全にメモリ内にあり、最小限のリソースを消費するデータベースがあるとします。 テストを実行する前に、データを入力する必要があります。SQLクエリを記述するよりも普遍的な方法でこれを行うことをお勧めします。 また、データベースの操作後にデータベースの状態を確認する必要があります。 そこからデータを取得し、それを参照と手動で比較するには、アイデアが非常に悪いことがわかります。 そのため、操作の結果の初期化と検証の問題を解決するために、データベースの初期化とその後のデータセットの検証の自動化に最適なDBUnitライブラリが作成されました。



使用例



HSQLDBバンドルとDBUnitバンドルの機能を示すために、コンストラクターがパラメーターとしてデータベースコネクターを受け入れるクラスを作成します。 このクラスには、テキスト文字列をパラメーターとして受け取り、それを個別の単語に分割し、データベーステーブル内の単語の出現頻度に関する統計を追加するメソッドが含まれます。 クラスは次のようになります。



public class User { private Connection sqlConnection; public User(Connection sqlConnectopn) { this.sqlConnection = sqlConnectopn; } private int insertOriginalString(String originalString) throws SQLException { int originalStringId = 0; PreparedStatement psInsert = sqlConnection. prepareStatement( "INSERT INTO original_strings (strings, date) VALUES (?, now())", PreparedStatement.RETURN_GENERATED_KEYS ); psInsert.setString(1, originalString); psInsert.execute(); ResultSet rsInsert = psInsert.getGeneratedKeys(); if(rsInsert.next()) { originalStringId = rsInsert.getInt(1); } else { throw new RuntimeException(); } rsInsert.close(); psInsert.close(); return originalStringId; } private int insertToken(int originalStringId, String token) throws SQLException { int tokenId = 0; PreparedStatement psTokenId = sqlConnection. prepareStatement("SELECT id FROM tokens WHERE word = ?"); psTokenId.setString(1, token); ResultSet rsToken = psTokenId.executeQuery(); if(rsToken.next()) { tokenId = rsToken.getInt(1); } else { PreparedStatement psInsertToken = sqlConnection. prepareStatement( "INSERT INTO tokens (word) VALUES (?)", PreparedStatement.RETURN_GENERATED_KEYS ); psInsertToken.setString(1, token); psInsertToken.execute(); ResultSet rsInserToken = psInsertToken.getGeneratedKeys(); if(rsInserToken.next()) { tokenId = rsInserToken.getInt(1); } else { throw new RuntimeException(); } rsInserToken.close(); psInsertToken.close(); } rsToken.close(); psTokenId.close(); return tokenId; } private void linkTokenToString(int originalStringId, int tokenId) throws SQLException { PreparedStatement psCreateLink = sqlConnection. prepareStatement("INSERT INTO links (original_string_id, token_id) VALUES(?,?)"); psCreateLink.setInt(1, originalStringId); psCreateLink.setInt(2, tokenId); psCreateLink.execute(); } public void logRequestString(String requestString) throws SQLException { String preParsed = requestString.replaceAll("\\W+", " "); String[] tokens = preParsed.split(" "); if(tokens.length > 0) { int originalStringId = insertOriginalString(requestString); for(String token: tokens) { linkTokenToString( originalStringId, insertToken(originalStringId, token) ); } } } }
      
      







次に、単体テストを作成します。



 public class UserTest { private IDatabaseTester tester = null; @Before public void instantiate() throws Exception { //Creating databse server instance tester = new JdbcDatabaseTester("org.hsqldb.jdbcDriver", "jdbc:hsqldb:mem:" + UUID.randomUUID().toString(), "sa", ""); //Creating tables tester.getConnection().getConnection().prepareStatement("CREATE SEQUENCE SEQU AS INTEGER START WITH 0").execute(); tester.getConnection().getConnection().prepareStatement("CREATE SEQUENCE SEQU2 AS INTEGER START WITH 0").execute(); tester.getConnection().getConnection().prepareStatement("CREATE SEQUENCE SEQU3 AS INTEGER START WITH 0").execute(); tester.getConnection().getConnection().prepareStatement("CREATE TABLE TOKENS(ID INT GENERATED BY DEFAULT AS SEQUENCE SEQU NOT NULL PRIMARY KEY, WORD LONGVARCHAR NOT NULL)").execute(); tester.getConnection().getConnection().prepareStatement("CREATE TABLE ORIGINAL_STRINGS(ID INT GENERATED BY DEFAULT AS SEQUENCE SEQU2 NOT NULL PRIMARY KEY, STRINGS LONGVARCHAR NOT NULL,DATE TIMESTAMP NOT NULL)").execute(); tester.getConnection().getConnection().prepareStatement("CREATE TABLE LINKS(ID INT GENERATED BY DEFAULT AS SEQUENCE SEQU3 NOT NULL PRIMARY KEY,TOKEN_ID INT NOT NULL,ORIGINAL_STRING_ID INT NOT NULL)").execute(); //Setting DATA_FACTORY, so DBUnit will know how to work with specific HSQLDB data types tester.getConnection().getConfig().setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new HsqldbDataTypeFactory()); //Getting dataset for database initialization IDataSet dataSet = new FlatXmlDataSetBuilder().build(this.getClass().getClassLoader().getResourceAsStream("template_set.xml")); //Initializing database tester.setDataSet(dataSet); tester.onSetup(); } @Test public void logRequestStringTest() throws SQLException, Exception { User man = new User(tester.getConnection().getConnection()); man.logRequestString("Hello, world!"); ITable template = new FlatXmlDataSetBuilder().build(this.getClass().getClassLoader(). getResourceAsStream("check_set.xml")).getTable("tokens"); ITable actual = DefaultColumnFilter.includedColumnsTable(tester.getConnection().createDataSet().getTable("tokens"), template.getTableMetaData().getColumns()); Assertion.assertEquals(template, actual); } }
      
      







データセットファイルは次のとおりです。



template_set.xml

 <dataset> </dataset>
      
      







check_set.xml

 <tokens WORD="Hello" /> <tokens WORD="world" />
      
      







単体テストを表示すると、すぐに疑問が生じる場合があります。「データベースにテーブルを作成するためのコードが単体テストに含まれているのはなぜですか?! 彼らはファイルからデータセットをダウンロードすると約束しましたか?」 はい、そうです。ファイルからセットを読み込みますが、xmlを使用してデータベース構造を記述し、すべてのデータベースドライバーで動作させることは、各DBMSのDDLクエリの構文が異なるため、簡単なプロセスではありません。 したがって、このような機能はDBUnitでは使用できません。

次の設計に注意を向けたいと思います。

 ITable actual = DefaultColumnFilter.includedColumnsTable(tester.getConnection().createDataSet().getTable("tokens"), template.getTableMetaData().getColumns());
      
      





DefaultColumnFilter



関数は、レコードのIDを考慮せずにデータセットを比較するために列をフィルタリングします。



おわりに



この記事では、データベースを操作する最も単純な例を分析しました。 読者は、このようなテストへのアプローチがODBC接続の「裸の」使用だけでなく、ORMフレームワークにも適用可能であることを容易に想像できます。 あなたのための安定したコード!



All Articles