JavaEEでもTDD - JPA編 - 2
こっちの続きです。
環境構築はできたと思うので、本命のTDD入ります。まだの人は前回の記事見るか、githubからできたものをを落としてください。
1. 記事の投稿をしよう!
まずは、記事の投稿機能を実装します。現在、ArticleにはIdしか存在しないので、タイトルとコンテンツを追加します。修正箇所はこんな感じ。
次に、ArticleFacadeTestに下記のようにテストを追加します。
@Test public void save_and_select_Test() throws Exception { utx.begin(); // init and check. assertThat(articleFacade.count(), is(0)); // action. articleFacade.create(new Article(null, "title1", "contents1")); assertThat(articleFacade.count(), is(1)); articleFacade.create(new Article(null, "title2", "contents2")); assertThat(articleFacade.count(), is(2)); // expected. List articles = simpleSort(articleFacade.findAll(), "Title"); assertThat(articles.size(), is(2)); assertThat(articles.get(0).getTitle(), is("title1")); assertThat(articles.get(0).getContents(), is("contents1")); assertThat(articles.get(1).getTitle(), is("title2")); assertThat(articles.get(1).getContents(), is("contents2")); utx.commit(); }
記事の保存と削除を確認しています。simpleSortというのは親クラスに定義してるメソッドです。RDBはorder byをかけない限り、取得順序が保証されないのでテストが書きづらいという問題があります。なので、取得したListを第二引数でsortするのがsimpleSortメソッドになります。軽く書けるようにリフレクションとか使ってるのでデータ量が増えると結構微妙なコードかもです。まあ、TDDではそんなにデータ使わないはずなのでOK, OK.
あと、これだけだと、何度もテストすると結果が変わってしまうので、下記の初期化コードを追加します。
@Before public void preparePersistenceTest() throws Exception { clearData(Article.class); }
clearDataは親クラスのAbstractJPATestに定義してあるメソッドです、指定したEntityクラスのテーブルを削除します。Beforeで指定してあるので、これでテスト毎にクリーンな状態で動作確認ができます。 これらのコードを追加して、テストを実行してみましょう。GreenになればOKです。この辺はJPAがもとから持ってる機能なので、本来はテスト不要な箇所だと思います。
2. 記事を更新しよう!
今度はアップデートのテストです。これもJPAの機能の確認なのでテストコードのみの実装となります。
@Test public void update_Test() throws Exception { utx.begin(); // init and check. load(); List articles = simpleSort(articleFacade.findAll(), "Title"); assertThat(articles.size(), is(2)); assertThat(articles.get(0).getTitle(), is("title1")); assertThat(articles.get(1).getTitle(), is("title2")); // action articles.get(0).setTitle("title3"); articleFacade.edit(articles.get(0)); articleFacade.edit(new Article(null, "title4", "contents4")); // expected List updatedArticles = simpleSort(articleFacade.findAll(), "Title"); assertThat(updatedArticles.size(), is(3)); assertThat(updatedArticles.get(0).getTitle(), is("title2")); assertThat(updatedArticles.get(1).getTitle(), is("title3")); assertThat(updatedArticles.get(2).getTitle(), is("title4")); utx.commit(); }
また、初期データの登録用にloadというメソッドを定義しました.
private void load() throws Exception { articleFacade.create(new Article(null, "title1", "contents1")); articleFacade.create(new Article(null, "title2", "contents2")); }
こちらも実行するとGreenになることが確認できるかと思います。
3. タイムスタンプを追加しよう!
さて、ようやく少し面白くなってきます。Articleに作成日と更新日の2つのTimestampを追加したいと思います。まずは、ArticleにcreatedAtとupdatedAtの2つのプロパティを追加します。差分はこちらの通り。
注目は@Temporalと@PrePersist, @PreUpdateです。@Temporalは日付などに指定するアノテーションです。引数の内容で精度を変更することができます。PrePersistとPreUpdateはトリガーと呼ばれているもので、名前の通り、Entityの更新や作成などのイベントに反応してその前に必ず実行されるメソッドを定義することができます。
イベントドリブンでそいう処理が書けるのは中々便利です。ストアド使うまでもないときには特に。今回はここで、タイムスタンプの更新をかけています。
では、その動作を確認しましょう。
@Test public void update_timestamp_Test() throws Exception { utx.begin(); // init and check. load(); List articles = simpleSort(articleFacade.findAll(), "Id"); assertThat(articles.size(), is(2)); assertThat(articles.get(0).getCreatedAt(), is(notNullValue())); assertThat(articles.get(0).getUpdatedAt(), is(notNullValue())); Long createdAt1 = articles.get(0).getCreatedAt().getTime(); Long updatedAt1 = articles.get(0).getUpdatedAt().getTime(); Long createdAt2 = articles.get(1).getCreatedAt().getTime(); Long updatedAt2 = articles.get(1).getUpdatedAt().getTime(); // action articles.get(0).setTitle("title3"); articleFacade.edit(articles.get(0)); articleFacade.edit(new Article(null, "title4", "contents4")); // expected List updatedArticles = simpleSort(articleFacade.findAll(), "Id"); assertThat(updatedArticles.size(), is(3)); // record1 assertThat(updatedArticles.get(0).getCreatedAt().getTime(), is(createdAt1)); assertThat(updatedArticles.get(0).getUpdatedAt().getTime(), is(greaterThan(updatedAt1))); // record2 assertThat(updatedArticles.get(1).getCreatedAt().getTime(), is(createdAt2)); assertThat(updatedArticles.get(1).getUpdatedAt().getTime(), is(updatedAt2)); utx.commit(); }
こちらでGreenになったのが確認できたと思います。確認内容は作成時にちゃんと時間が入ることと、更新されると更新日だけがアップデートされることです。
よく考えたら、これはトリガーの実装前にちゃんとテストで赤になることを確認したほうが良かったですね。失敗。まあ、次から改善したいです。
4. 最新の記事の一覧を取得しよう!
では、次は最新の記事の一覧を取得する機能を実装します。テストはこんな感じで。
@Test public void find_recent_articles_Test() throws Exception { utx.begin(); // init and check. assertThat(articleFacade.count(), is(0)); for (int i = 0; i < 100; i++) { SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd"); Article article = new Article(null, "title" + i, "contents" + i); article.setCreatedAt(df.parse("2012/04/1")); article.setUpdatedAt(df.parse*1; articleFacade.create(article); } assertThat(articleFacade.count(), is(100)); // expected. List articles = simpleSort(articleFacade.findRecently(10), "Title"); assertThat(articles.size(), is(10)); int i = 90; for (Article article : articles) { assertThat(article.getTitle(), is("title" + (i++))); } utx.commit(); }
今度はちゃんと先にテストを書きました。では、実行してみましょう。
「わーい、お目々が...じゃない、テストが真っ赤だー」という感じにちゃんとRedになりましたね。まあ、findRecentlyなんてメソッド定義してませんからね。では、fake itを書きます。とりあえず、こんな感じに書きました。
さて、早速実行してみます。Greenですね! では、テストを追加しましょう。
List articles20 = simpleSort(articleFacade.findRecently(20), "Title"); assertThat(articles20.size(), is(20)); int j = 80; for (Article article : articles20) { assertThat(article.getTitle(), is("title" + (j++))); }
これを実行すると、ちゃんとテストが赤くなるのが確認できたと思います。
赤くなったので、今度は正しいコードを実装します。JPQLでこちらのように実装しました。何気にfake itより短いですwww
実行すると無事、Greenになることを確認出来ました。
5. コメントを追加しよう!
ブログを書いたらやっぱり反応がほしいので、コメント機能を実装します。
まずは、CommentのエンティティとDAOを作成します。こんな感じ。とりあえずはデフォルト生成のままです。
続いてテストは下記の通り。
@Test public void comment_add_Test() throws Exception { // init and check. utx.begin(); Article article = new Article(1L, "title1", "contents1"); articleFacade.create(article); Comment comment = new Comment(null, "user2", "comment2"); comment.setArticle(article); commentFacade.create(comment); utx.commit(); // expected. utx.begin(); List<>Article> articles = simpleSort(articleFacade.findAll(), "Title"); assertThat(articles.size(), is(1)); assertThat(articles.get(0).getId(), is(1L)); assertThat(articles.get(0).getTitle(), is("title1")); Comment comment2 = commentFacade.findAll().get(0); assertThat(comment2.getName(), is("user2")); assertThat(comment2.getArticleId(), is(1L)); assertThat(comment2.getArticle().getTitle(), is("title1")); assertThat(comment2.getArticle().getId(), is(1L)); utx.commit(); }
ArticleとCommentを登録して1:Nで双方向マッピングができているかを確認しています。あと、フィールドとか細かく変わってるので詳細はこちらを参考に。これを実行するとRedになります。Commentにarticleが無いので当然ですね。
まずは、articleフィールドだけ追加します。コンパイルエラーは取れましたが、nullが返ってきているので、テストがまだ失敗しています。これは、フィールドはあるものの、マッピングが正しく行われていないためです。
つづいて、@ManyToOneとか@OneToManyとかマッピング系の設定を追加します。追加したものはこちら。
こちらを実行するとGreenになります。まだ、あまりJPAに慣れていないので、記述が適切かはちょっと怪しいです。でもまあ、テストがあるので必要に応じて修正できますね。ビバ☆ノットレガシー><
まとめ
JPAとArquillianの勉強にTDDな感じで進めて見ました。色々、間違ってるところとかあるかもしれないので、その辺はツッコミお待ちしております。
てか、JPAは今回テスト書きながらトランザクション周りが自分の直感とかなり異なってることに気づいたので、本気で調べとかないと死にそうな気がしてきました...
あと、今回は素のArquillianを使いましたが、JPAを使う場合は@aslakknutsenにTweetもしてもらったので、Arquillian Persistence Extensionも試してみたいと思います。
なお、今回作成したものはgithubに置いてあるのでご自由にどうぞ。
では、Happy Hacking !