読者です 読者をやめる 読者になる 読者になる

PlayFrameworkでセッションID、DBのデータが必要なFunctionalTest

たぶんFunctionalTestの使い方が間違ってますが一応書こうと思います。

まずセッションIDについて。
開発中のアプリで、セッションにモデルのIDを保存しておくメソッドと、そのIDを後から使うメソッドがあります。
簡略化して書くと下のようなコードです。実行時には問題ないのですが、FunctionalTestだと、同じテストの中でfooとbarを呼び出していても、SessionIdがリクエストごとに違うので、barを呼び出したときには"[sessionId]-mymodel"はNullになってしまいます。

	public static void foo(){
		MyModel model = new MyModel();
		// いろいろ処理
		model.save();
		
		// セッション(クッキー)にIDを保存しておく
		String sessionId = session.getId()+"-mymodel";
		session.put(sessionId, model.id);
		render();
	}
	
	public static void bar(){
		Long myModelId = Long.parseLong(session.get(session.getId()+"-mymodel")); //これがNullになる
		// いろいろな処理
		render();
	}

fooとbarの間で同じセッションIDを使わせるために、こんな形にしました。

    	HashMap params = new HashMap();
    	params.put("param1", "test1");
    	params.put("param2", "test2");
    	Response response = POST("/foo", params);
    	
	Request req = newRequest();
	req.url = "/bar"; // urlを指定しないとだめらしいです。
	req.path = "/bar";
	req.cookies.put("PLAY_SESSION", response.cookies.get("PLAY_SESSION")); // fooと同じセッションを使う
        response = POST(req, "/bar");

他にもいろいろと試してみましたが、結局どれもうまくいかず、上の方法に落ち着きました。
ちなみに下の方法だとテストが1つだけならうまくいくものの、別の同じようなCookieを使うテストを追加したとたんに java.util.concurrent.ExecutionException: play.exceptions.JavaExecutionExceptionが発生しました。

	Request req = newRequest();
	req.url = "/bar";
	req.path = "/bar";
	Cookie c1 = new Cookie();
	c1.name = "PLAY_SESSION"; 
	c1.value = "10d4f52bc60ddf2c65769b4b73b3ffa9fae988eb-%00___ID%3A7fcac13a-d06f-4d3a-b417-827c0c24a5d8%00%007fcac13a-d06f-4d3a-b417-827c0c24a5d8-mymodel%3A"+myModel.id+"%00";
	req.cookies.put("PLAY_SESSION", c1);

それからDB関係の操作でもいろいろとFunctionalTestを作る中でハマッたのでメモします。
テストの中でモデルを作成したり更新したりして、Controllerのアクションを呼び出して結果をAssertする、というようなことをすると、モデルのデータが意図したものになっていなかったり、「play.exceptions.JPAException has been caught, The JPA context is not initialized」などのエラーが出ました。

間違ってたらツッコミをお願いしたいのですが、これはたぶんJPAがFunctionalTestからは独立して存在しているからなのだと思います。FunctionalTestの中でテスト用のモデルを作ったり、Fixtureを使っていても、それが意図した時点でDBにきちんと反映されているとは限らない、とうことなのだと思います。FunctionalTestの中でJPAを使うときには、明示的にJPA ContextをInitializeして、いつトランザクションを開始するのか、いつ終了するのか、といったことを指定する必要があるのだと思います。

具体的にはメソッドの中でモデルをいじってどうのこうのする場合は、下の記述を入れないとだめでした。

	@Before
	public void setUp() throws Exception {
	    new Job() {
	        @Override
	        public void doJob() throws Exception {
	        	Fixtures.deleteAllModels(); 
	        	Fixtures.loadModels("data.yml");
	        }
	    }.now().get();
	}
	
	@Test
	public void someTest(){
		// 引数はRollbackするかどうか
		JPAPlugin.startTx(false); 
		// このあたりでDBへの処理など
		// テスト用の送信Requestを作る
		Request req = newRequest();
		req.url = "/save"; 
		req.path = "/save";
    		req.params.put("param1", "Test");
    		req.params.put("param2", "Test");
		// Controllerのアクションを呼び出し
		POST(req, "/save");
		// DBからUpdateされた結果がほしいときにはclearを呼び出す(?)
		JPA.em().clear(); 
		MyModel m = (MyModel)MyModel.findById(id);
		// トランザクションを終了
		JPAPlugin.closeTx(false);
		
		assertEquals("Success!", m.resultText);
	}

UnitTestの時に「JPA context is not initialized」エラーが出ないのは、モデルには@Entityアノテーションがあるので、最初の呼び出し時にJPAが自動でInitializeされているからだと思います。試しにモデルのUnitTestの中でJPAPlugin.startTx(false)、JPAPlugin.closeTx(false)を記述すると、それ以降のテストではJPA contetxt is not Initializedのエラーが出ます。

それからOneToManyなどで子としてCollectionを持っている場合は、モデルを取得した後にCollection
にアクセスしようとすると「A org.hibernate.LazyInitializationException has been caught, failed to lazily initialize a collection of role: models.MyModel.children no session or session was closed」というエラーが発生する場合があります。子になっているCollectionはiteratorにアクセスがあるまでは、実際にはDBから取得されないようです。なのでmodel.children.iterator()を、Collectionを使う前に呼び出しておくと、ちゃんとロードされます。これはトランザクションの終了前(JPAPlugin.closeTxの前)に行う必要があります。

いろいろあやふやな部分がありますが、いまの理解ではこんなところです。

Sources:
http://openejb.apache.org/jpa-concepts.html
http://www.playframework.org/documentation/api/1.2.5/play/db/jpa/JPAPlugin.html
http://docs.oracle.com/javaee/5/api/javax/persistence/EntityManager.html
http://www.playframework.org/documentation/1.2.4/model#stateless
http://stackoverflow.com/questions/7593802/functional-test-in-playframework-fails-when-adding-items-to-cart