お題
前回、GAE用のJava8アプリをMaven自動生成した際にできるテストコードについて触れた。
今回は、テストファーストを意識して、Datastoreアクセスロジックの追加を試みる。
GAE試行Index
- GAE/Java8試行(その0:「App Engineについて」)
- GAE/Java8試行(その1:「Java8でWebアプリ作ってデプロイ」)
- GAE/Java8試行(その2:「Javaアプリ解説」)
- GAE/Java8試行(その3:「Javaアプリテストコード解説」)
開発環境
# OS
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="17.10 (Artful Aardvark)"
# Java
$ java -version
java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)
# IDE
みんな大好きIntelliJ IDEA
参考
実践
■設計
これから実装する機能の仕様を決める。
テーマは、GCPのチュートリアルでもよくある「書籍リストの管理」システムとする。
今回Datastoreへのアクセスロジックを試す機能としては、
「書籍名」をPOSTしたらDatastoreに登録されるものとする。
■テストコード
仕様に合わせて、下記のようなテストコードを書いてみる。
モックリクエストを操作し、「書籍名」がリクエストパラメータから取得できるようにする。
そして、 doPost(~~)
実行で上記の「書籍名」が book
カインドに登録されていることを検証する。
@Test
public void 書籍名をPOSTするとDatastoreに登録される() throws EntityNotFoundException, ServletException, IOException {
/*
* SetUp
*/
Map<String, String[]> parameterMap = new HashMap<>();
parameterMap.put("bookName", new String[]{"マイクロサービスアーキテクチャ"});
when(mockRequest.getParameterMap()).thenReturn(parameterMap);
/*
* Execute
*/
servletUnderTest.doPost(mockRequest, mockResponse);
/*
* Assert
*/
DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
Entity e = ds.get(KeyFactory.createKey("book", 1));
String microService = (String)e.getProperty("bookName");
assertThat(microService).isEqualTo("マイクロサービスアーキテクチャ");
}
ちなみに、テストファーストなので doPost(~~)
のない状態では上記テストコードはコンパイルエラー。
その後、 doPost(~~)
の定義だけ書いて、テストコードを実行すると、下記のようになる。
リクエストパラメータとして渡した「書籍名」をDatastoreに登録するロジックは未実装なので、当然の結果。
テストファーストでは、まず、仕様を決めて、それを確認するテストコードを書く。
(その時点ではコンパイルも通らないが、そこから始めることが重要)
最初にテストに失敗させる。
失敗することがわかっているのに、あえて失敗させる。ロジック実装前なので、テスト実行結果がNGとなる。
これを最初に確認することで、ロジック実装後にテスト実行結果がOKとなることに意味が出る。
(最初に失敗することを確認しないと、正しい検証コードが書けていなくて、そもそも最初からテスト実行結果がOKだったかもしれず、正しく実装できていることをテストできていないテストコードになる可能性があるため)
com.google.appengine.api.datastore.EntityNotFoundException: No entity was found matching the key: book("micro")
at com.google.appengine.api.datastore.BaseAsyncDatastoreServiceImpl$1.wrap(BaseAsyncDatastoreServiceImpl.java:174)
at com.google.appengine.api.datastore.BaseAsyncDatastoreServiceImpl$1.wrap(BaseAsyncDatastoreServiceImpl.java:169)
at com.google.appengine.api.utils.FutureWrapper.wrapAndCache(FutureWrapper.java:56)
at com.google.appengine.api.utils.FutureWrapper.get(FutureWrapper.java:93)
at com.google.appengine.api.datastore.FutureHelper.getInternal(FutureHelper.java:76)
at com.google.appengine.api.datastore.FutureHelper.quietGet(FutureHelper.java:63)
at com.google.appengine.api.datastore.DatastoreServiceImpl.get(DatastoreServiceImpl.java:41)
at com.example.sky0621.HelloAppEngineTest.書籍名をPOSTするとDatastoreに登録される(HelloAppEngineTest.java:100)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
10 23, 2018 12:21:13 午前 com.google.appengine.api.datastore.dev.LocalDatastoreService cleanupActiveServices
情報: scheduler shutting down.
Disconnected from the target VM, address: '127.0.0.1:37713', transport: 'socket'
Process finished with exit code 255
さて、想定通りのテスト失敗となったので、いよいよDatastore登録ロジックを実装する。
■実装
こんな感じ。
プロダクトコードで使えるレベルではないけど、とりあえずこれだけでリクエストパラメータから「書籍名」を捕まえてDatastoreni登録するコードになる。
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
req.getParameterMap().forEach((k, v) -> {
Key key = KeyFactory.createKey("book", 1);
Entity e = new Entity(key);
e.setProperty("bookName", Arrays.stream(v).collect(Collectors.joining()));
ds.put(e);
});
}
この実装後に先ほどのテストコードを流すと、今度はテスト結果OKとなる。
まとめ
テストファーストでやると、ちょっとした機能の実装だけでも、それなりに時間がかかるようになる。
ただ、”それなりの時間”には、仕様の検討、実装、単体テスト、リファクタ、手戻り削減コストなどなどが含まれているので、本当のところはコスパ的には良い。