GAE/Java8試行（その4：「Datastoreへのアクセスロジック」）

# お題
[前回](https://qiita.com/sky0621/items/874c8b6462fdc5a0829d)、GAE用のJava8アプリをMaven自動生成した際にできるテストコードについて触れた。
今回は、テストファーストを意識して、Datastoreアクセスロジックの追加を試みる。

# GAE試行Index
- [GAE/Java8試行（その0：「App Engineについて」）](https://qiita.com/sky0621/items/6175d578d0521858a944)
- [GAE/Java8試行（その1：「Java8でWebアプリ作ってデプロイ」）](https://qiita.com/sky0621/items/8437da4e96bffef69250)
- [GAE/Java8試行（その2：「Javaアプリ解説」）](https://qiita.com/sky0621/items/65ce0406fb895f547ae5)
- [GAE/Java8試行（その3：「Javaアプリテストコード解説」）](https://qiita.com/sky0621/items/874c8b6462fdc5a0829d)

# 開発環境
### # 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](https://www.jetbrains.com/idea/)

# 参考
- [データストアと Memcache のテストを作成する](https://cloud.google.com/appengine/docs/standard/java/tools/localunittesting?hl=ja#datastore-memcache)
- [Cloud Datastore の概要](https://cloud.google.com/appengine/docs/standard/java/datastore/?hl=ja)
- [エンティティ プロパティ リファレンス](https://cloud.google.com/appengine/docs/standard/java/datastore/entity-property-reference?hl=ja)

# 実践

## ■設計
これから実装する機能の仕様を決める。
テーマは、GCPのチュートリアルでもよくある「書籍リストの管理」システムとする。
今回Datastoreへのアクセスロジックを試す機能としては、
「書籍名」をPOSTしたらDatastoreに登録されるものとする。

## ■テストコード

仕様に合わせて、下記のようなテストコードを書いてみる。
モックリクエストを操作し、「書籍名」がリクエストパラメータから取得できるようにする。
そして、 `doPost(~~)` 実行で上記の「書籍名」が `book` カインドに登録されていることを検証する。

```java
  @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("マイクロサービスアーキテクチャ");
  }
```

https://github.com/sky0621/java-webapi-for-gae-study/blob/master/sky0621/src/test/java/com/example/sky0621/HelloAppEngineTest.java

ちなみに、テストファーストなので `doPost(~~)` のない状態では上記テストコードはコンパイルエラー。
その後、 `doPost(~~)` の定義だけ書いて、テストコードを実行すると、下記のようになる。
リクエストパラメータとして渡した「書籍名」をDatastoreに登録するロジックは未実装なので、当然の結果。
テストファーストでは、まず、仕様を決めて、それを確認するテストコードを書く。
（その時点ではコンパイルも通らないが、そこから始めることが重要）
最初にテストに失敗させる。
失敗することがわかっているのに、あえて失敗させる。ロジック実装前なので、テスト実行結果がNGとなる。
これを最初に確認することで、ロジック実装後にテスト実行結果がOKとなることに意味が出る。
（最初に失敗することを確認しないと、正しい検証コードが書けていなくて、そもそも最初からテスト実行結果がOKだったかもしれず、正しく実装できていることをテストできていないテストコードになる可能性があるため）

```java

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登録するコードになる。

```java

  @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);
    });
  }
```

https://github.com/sky0621/java-webapi-for-gae-study/blob/master/sky0621/src/main/java/com/example/sky0621/HelloAppEngine.java

この実装後に先ほどのテストコードを流すと、今度はテスト結果OKとなる。

# まとめ
テストファーストでやると、ちょっとした機能の実装だけでも、それなりに時間がかかるようになる。
ただ、”それなりの時間”には、仕様の検討、実装、単体テスト、リファクタ、手戻り削減コストなどなどが含まれているので、本当のところはコスパ的には良い。