Guiceが生成するインスタンスをMockitoでspyする

Play FrameworkではGoogle Guiceが標準で採用されている。 統合テストを書いているとGuiceが生成するインスタンスspyに差し替えたくなる時がごく稀にある。 この場合、Injector.getInstance()が返したインスタンスspyにするのではなく、Injector.getInstance()インスタンスを生成する処理の中でspyにしてやらないと、テスト対象の依存関係の中にspyが組み込まれない。

Injectorがインスタンスを生成する処理をカスタマイズしたい場合、インスタンスの生成手順をメソッドで定義する@Provides Methodsや、インスタンスを生成するメソッドを持つクラスを定義するProviderが使える。 @Provides Methodsを使った場合は本番用のコードの中にテストのためのコードを追加しなければならなくなるため、今回の目的ならテストコードを書くディレクトリでProviderを定義してやる方がいいだろう。

package com.pkinop.example

import com.google.inject.Injector
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.{doAnswer, spy, times, verify}
import org.mockito.invocation.InvocationOnMock
import org.mockito.stubbing.Answer
import org.scalatest.TestData
import org.scalatestplus.play.PlaySpec
import org.scalatestplus.play.guice.GuiceOneAppPerTest
import play.api.Application
import play.api.inject.bind
import play.api.inject.guice.GuiceApplicationBuilder

import javax.inject.{Inject, Provider, Singleton}

trait Reverser {
  def reverse(x: Int): Int
}

class ReverserImpl @Inject()() extends Reverser {
  def reverse(x: Int): Int = -x
}

class ReverserProvider @Inject()(injector: Injector)
  extends Provider[Reverser] {
  override def get(): Reverser = spy(
    injector.getInstance(classOf[ReverserImpl])
  )
}

class ReverserClient @Inject()(reverser: Reverser) {
  def reverse(x: Int): Int = reverser.reverse(x)
}

class ReverserSpec
  extends PlaySpec
  with GuiceOneAppPerTest {
  override def newAppForTest(testData: TestData): Application =
    GuiceApplicationBuilder().overrides(
      // SingletonにしておかないとReverserを要求する場所ごとに別のspyが生成されてしまい
      // 動作の変更や呼び出しの検証を行うspyが依存関係の解決に使われなくなってしまう
      bind[Reverser]
        .toProvider(classOf[ReverserProvider])
        .in[Singleton]
    ).build()

  "呼び出しをテストする" in {
    val reverserClient = app.injector.instanceOf[ReverserClient]

    val result = reverserClient.reverse(1)

    result mustBe -1
    val reverser = app.injector.instanceOf[Reverser]
    verify(reverser, times(1))
      .reverse(1)
  }

  "実装を変更してみる" in {
    val reverser = app.injector.instanceOf[Reverser]
    doAnswer(new Answer[Int] {
      override def answer(invocation: InvocationOnMock): Int = {
        val original = invocation.callRealMethod().asInstanceOf[Int]
        original * 10
      }
    }).when(reverser).reverse(any)
    val reverserClient = app.injector.instanceOf[ReverserClient]

    val result = reverserClient.reverse(1)

    result mustBe -10
  }
}

実際にReverserClientReverserインスタンスを要求すると、ReverserImplspyが依存関係の解決に使われているのがわかるだろう。 ProviderもGuiceで生成でき、Providerの中でもGuiceによる依存関係の解決ができるというのがミソだ。

なおこの方法では具象クラスのインスタンスを要求している箇所でspyに差し替えることはできない。 ここでわざわざReverserReverserImplを用意しているのもそれが理由だ。 仮にReverserというクラスを用意してそのまま実装を書くと、以下のようになる。

class Reverser @Inject()() extends Reverser {
  def reverse(x: Int): Int = -x
}

class ReverserProvider @Inject()(injector: Injector)
  extends Provider[Reverser] {
  override def get(): Reverser = spy(
    injector.getInstance(classOf[Reverser])
  )
}

すると、GuiceReverserインスタンスを生成するためにReverserProvider.get()を呼び出すが、その中ではGuiceを使ってReverserインスタンスを生成しているため循環参照に陥ってしまう。

もっとも、spyに差し替えたくなるようなクラスはインターフェイスが用意されていることがほとんどだろうし、そうでなくてもインターフェイスを用意するだけであれば本番用のコードの中にテスト用の実装が入るわけではないから、テストのために本番コードを歪める行為の中ではだいぶマシな部類であり、この制限が問題になることはあまり無いだろう。

「単体テストの考え方/使い方」で得た知見をレガシーコードの多いプロジェクトで実践してみた感想

本書の感想だが、Railsでの開発で最初にRSpecの使い方を学んだきりテストについて全く学んでこなかった身としては、テストコードを書く際の指針、プラクティスがはっきりし、非常によい内容だった。 その上で、学んだ内容を実際にプロジェクトで実践しようとした際、レガシーコードまみれのプロジェクトであることが起因して色々と問題があった。

私が関わっているプロジェクトで単体テストを書けるコードはほぼ無い

私がこの本を読んで得た一番の学びの1つがこれだった。 単体テストの定義をおさらいすると

  • 「単体」と呼ばれる少量のコードを検証する
  • 実行時間が短い
  • 隔離された状態で実行される

の3つを満たすテストコードが単体テストとなる。 この内1つ目と3つ目の定義には解釈の余地があり、その解釈をめぐってデトロイト学派とロンドン学派という2つのスタイルが存在している。

この本の著者は古典学派であり、古典学派の立場では「単体」とはビジネス上の単体のふるまいを指し、「隔離」というのはそれぞれのテストケースが他のテストケースから隔離された状態で実行されるということを意味する。

ここで私が現在携わっているプロジェクトについて話をしなければならないのだが、以下のような残念な状態になっている。

  • アプリケーション外部への副作用を持つ依存関係が何個もあるクラスに複雑なロジックが書かれている
    • 酷いときにはControllerからRepositoryを呼び出してそのままロジックが始まったりする
  • domainというパッケージの中には、DBのテーブル構造を写しただけの何のメソッドも持たない構造体のようなものが入ってる
  • DBのテーブル1つに対しRepositoryが作られており、集約はほとんど活用されていない

この本では、それぞれのクラスを「依存関係が多い/少ない」, 「コードが複雑/複雑でない」という2つの軸で評価できるとしており、それぞれのクラスは以下のように名前が付けられている。

依存関係が少ない 依存関係が多い
コードが複雑 ドメインモデル/アルゴリズム 過度に複雑なコード
コードが複雑でない 取るに足らないコード コントローラ

この内、過度に複雑なコードはテストしないと危険だが、依存関係が多いために簡単にはテストができないという厄介なものだ。 このようなクラスはコードの複雑さが高いが依存関係をほとんど持たない「ドメインモデル/アルゴリズム」と、依存関係を多く持つがコードの複雑さがほとんどない「コントローラ」に分離する必要がある。 、依存関係が多くロジックも複雑なクラスはテストしないと危険だがテストが困難であるため、依存関係が多いが複雑なロジックを持たないクラスと、ロジックが複雑だがアプリケーション外部への副作用を持つ依存関係は持たないクラスに分ける必要があると説いている。 そして、依存関係が少なくテストしやすい「ドメインモデル/アルゴリズム」を単体テストで、依存関係が多いが複雑なロジックを持たない「コントローラ」は必要があれば統合テストによりカバーしていく。

ここで私のプロジェクトを振り返ってみると、「ドメインモデル/アルゴリズム」に書かれるべきだったビジネスロジックの大半が「コントローラ」に流れ出てしまい、「過度に複雑なコード」が大量に生まれてしまっている状態だ。 ビジネスロジックが漏れ出てしまったドメインモデルのなり損ないについては「取るに足らないコード」に分類できる。 このような状況では、実行時間が短く他のテストケースから隔離された状態で実行される単体テストは書きようがない。 リファクタリングの前に追加されるテストは統合テストにならざるを得ない。

レガシーコードまみれのプロジェクトではリファクタ耐性を意識して統合テストを追加していくしかない

ではこの本を読んだのは無駄だったのかと言えば、「単体テストの考え方/使い方」というタイトルに反して統合テストのプラクティスについても書いてあったので全く無駄ではなかった。 本書では、質のいい単体テストの性質として以下の4つを挙げている。

残念ながら、これら4つの要素の内最初の3つは互いにトレードオフの関係になってしまう。 実際に単体テストを作成する際はリファクタリング耐性を重要視しつつも、退行への保護と迅速なフィードバックへの間でバランスを取る必要がある。 統合テストは、これの内3, 4つ目の性質を犠牲にし、代わりにより強い対抗への保護, リファクタリング耐性を獲得したテストだと考えられる。 どうしても実行時間がかさむので迅速なフィードバックは得られなくなるし、セットアップなども長くなりがちなのでテストコードは単体テストより長くなり、その分保守性は低くなってしまう。

レガシーコードは必然的にテストで保護された後でリファクタリングを受けるので、退行への保護はもちろんリファクタリング耐性が極めて重要になってくる。 リファクタリング耐性を落とす要因として、モックすべきでないクラスのモック化が挙げられる。 リファクタリングされていく内に、もともとモックしていたクラスの呼び出しが行われなくなり、モックの呼び出しをテストしていたテストケースが落ちる。 このようなテストの失敗は、テストが落ちた原因の調査に時間をとられる分、コンパイルエラーになるようなテストの壊れ方よりだいぶ性質が悪い。

ではどのようなクラスをモックすべきかとこの本は説いているのかというと「アプリケーションの管理下にないプロセス外依存だけをモックせよ」と書いている。 順に説明していくと、まず「プロセス外依存」とは、DBや外部APIファイルシステムなどプロセスに割り当てられたメモリの外の存在にアクセスすることを指す。 次に「アプリケーション管理下にない」とは、アプリケーションを経由する以外にアクセスする方法が用意されていることを意味する。 たとえばDBなら、そのアプリケーションからしか直接読み書きされないならアプリケーションの管理下にあるプロセス外依存だが、もしそのDBを他のアプリケーションも利用するのであれば、アプリケーションの管理下にないプロセス外依存となる。 外から利用されないアプリケーションの管理下にあるプロセス外依存は実装の詳細であると考えられ、実装の詳細をテストしてしまっているテストは壊れやすくなる。

これまで私は統合テストの実行時間削減のためにRepositoryをモックするか、そもそも前術の「DBのテーブル1つに対しRepositoryが作られており、集約はほとんど活用されていない」という事情があるためにRepositoryの設計自体がイケてなく作り直すことになるのだからDBのレコードを確認するテストを書くべきなのか悩んでいたが、この辺の話を読んで、統合テストは迅速なフィードバックなど考えずDBをそのまま使って、退行への保護とリタクタリング耐性に全振りすべきなのだと理解できた。

今後解決したい悩み

ただ、この本に書いてある内容は正常なテストピラミッドが構築されていることが前提になる。 テストの割合は単体テストが最も多く、統合テストはそれより少なく、最後にごくわずかなE2Eテストがあるべきというあれだ。 単体テストが書けずひたすら統合テストを追加しているのだから、当然歪なテストピラミッドになる。 するとテストの実行時間がどんどん膨らんでいく。

このままではCIでテストを回す際に困ってしまうというのもあるのだが、1回の実行に30秒くらいかかるテストスイートを頼りにリファクタリングを行うというのは実際やっていてあまり開発体験がよくない。 リファクタ後は統合テストをやめて単体テストできる部分は単体テストに切り替えていきたいが、なかなかそこまで手が回らないのが現状だ。 この点については今後プラクティスを探っていきたい。

「単体テストの考え方/使い方」第3部, 4部を読んで考えたこと

特に後半は細かいプラクティスの話が多くなり、どうしても人によって「そうかな?」と思わされる内容も多そうだった。 とはいえ大事な考え方も書かれているので一読の価値は間違いなくあると感じた。

前回 p-kino.hatenablog.com

repositoryのインターフェイスを用意するか

この本のテストでのDBの扱いに関しての主張は以下のようなものだった

  • 複雑なビジネスロジックを持つドメインモデルとプロセス外依存を扱うクラスの連携を指揮する「コントローラ」に分類されるコードに対して統合テストを書くべき
  • 統合テストはテストケースを同時実行できるようにしようとすると保守コストが大きくなってしまうため、単体テストと違いテストケースを隔離しなくともよく、実際のDBにアクセスしてもよい
  • テスト対象のアプリケーションを通してしかアクセスされないDBは「実装の詳細」だと考えられ、実装の詳細をモックしてしまうとテストは壊れやすくなりリファクタリングへの耐性を失うため、モックすべきでない
  • 統合テストは本番環境のアプリケーションと同じ処理を実行させることが大事なので、インメモリな実装に置き換えたrepositoryを代わりに使うこともすべきではない
  • 実装が1つしかなく、モックするわけでもないクラスはインターフェイスを用意すべきではない(YAGNI原則に違反するため)

特にインメモリでDBを模したrepositoryについては、私も「そのrepositoryの実装にミスがあったら破綻するのでは?」と比定寄りの考えを持っていた。 となると、たしかにusecase層がinfrastructure層に依存しないよう依存性逆転を行うという目的こそあるが、それも形骸化してしまいいよいよrepositoryのインターフェイスを作成する意味がよくわからなくなってくる。 ただ、私はそれでもusecase層からrepositoryを呼び出させることを意図しているのであればインターフェイスを用意すべきだし、そうでないなら用意しないべきではないかと思った。

生成方法が複雑なドメインオブジェクトはその生成過程をfactoryに隠蔽するが、infrastructure層にfactoryだけあっても使う側がそれに気付けないと意味がない。 しかしDomain層でドメインモデルと同じパッケージにfactoryのインターフェイスが置いてあれば「これを使えばいいんだな」とわかる。 Scalaなら、repositoryをfactory以外から呼び出してほしくない場合はinfrastructure層のパッケージの外に公開しないこともできる。

このように、インターフェイスはusecase層に操作を公開するという役割もあり、テストにおいてモックすることが望ましくないとはいえ必要に応じて用意しておいた方がいいのではないかという気がした。

テストで本番と違う実装を使う

ここで思い出してほしいのが、テストでは本番環境とまったく同じ方法でテスト対象のコードとやり取りをしなくてはならない、ということです。つまり、テストだからと言って特別なことが許されるわけではないのです。

単体テストの考え方/使い方 プロジェクトの持続可能な成長を実現するための戦略』, Vladimir Khorikov, マイナビ出版, 2022-12-28, 須田智之 訳, p.378

これは、テストのためにprivateメソッドを公開してしまうのは、実装の詳細とテストが結びついてしまい壊れやすいテストになってしまうためアンチパターンであるという文脈で書かれていたことだ。

privateメソッドの公開に限らずテストのために特別なことをやってしまわないよう気をつけたいが、必ずしもそういうわけには行かないのではないかと感じた。私は少し前にこのルールを破らなければならないことがあった。

つい最近、顧客の要望でakka-quartz-schedulerを使ってバッチ処理を実装する機会があった。 その際のバッチ処理の内容は以下のようなものだった。

class SampleBatchProcessService(
  repository: CustomerInfoRepository
) {
  def process(customerId: Long): Unit =
    repository
      .find(customerId)
      .foreach { config =>
        // DBの変更を含む色々な処理
      }
}

class CustomerInfoRepository {
  def find(customerId: Long): Option[CustomerInfo]
}

case class CustomerInfo(foo: Long, bar: String)

このSampleBatchProcessServiceがActorから呼び出されていた。 ここで、顧客から「バッチ処理をしばらく止めたくなることがあるかもしれない」と言われたので、バッチ処理が有効になっているかをTINYINTで持つbatch_process_enabledというカラムをCustomerInfoを構成するためのテーブルの一部に追加し、それが1でない場合はCustomerInfoRepository.find()で見つからないようにすることでバッチ処理を止められるようにした。

このバッチ処理だが、結構な高頻度で実行されており、バッチ処理が実行されている間にbatch_process_enabledの値を変更しなければならないことも十分考えられた。 その場合、途中で処理を止めてしまうとDBの中身がおかしな状態になってしまう可能性が無いとは言えないため、既に走っているバッチ処理については最後まで通常どおり実行し、次回以降の実行はbatch_process_enabled1に戻るまで行わないという動作になっていてほしい。

これは統合テストで見ておくシナリオだと思ったのだが、テストを書くにあたって1つ問題があった。 実際の手順を再現するのであればバッチ処理を行うActorにメッセージを送った直後にbatch_process_enabledの値を0にし、Actorから完了を伝えるメッセージが返ってきたらDBがバッチ処理を行なったあとの状態として正しい内容になっているかを確認すればよいのだが、テストコードではActorの処理がすぐ終わってしまい、なおかつバッチ処理の実行中にbatch_process_enabledが変わろうが変わるまいが期待するバッチ処理実行後のDBの状態は変わらないため、本当に

  • Actorがメッセージを受け取り、SampleBatchProcessServiceが呼び出され、CustomerInfoRepositoryからCustomerInfoが見つかる
  • batch_process_enabled0に変更される
  • SampleBatchProcessServiceの処理が最後まで実行される

という順番で処理されているかわからなかった。なので、以下のようにspyを使ってCustomerInfoRepositoryのふるまいを変更して注入した。

val spyRepository = spy(new CustomerInfoRepository())
doAnswer(new Answer[AnyRef] {
  override def answer(invocation: InvocationOnMock): AnyRef = {
    val info = invocation.callRealMethod()

    // batch_process_enabledを0にするコード

    info
  }
}).when(spyRepository).find(any)

一応invocation.callRealMethod()で実際の実装が使われるが、完全に同じ実装というわけには行かなかった。 そもそもこの本の主張からすれば、テスト対象のプロジェクト以外からアクセスされることが全くないDBは管理下にある依存なので、モックすること自体が望ましくない。

ただ、一方で「バッチ処理の実行中にバッチ処理が行われないよう切り替える」というのはいかにもそのうち1回は行われそうな操作であり、しかもそんなことが行われる場合は既に何か他の問題が起きていることも考えられる。 そのような状況で更にデータを壊すようなバグを引き起こしてしまうというのはなるべく避けたく、ぜひとも統合テストでカバーしておきたいシナリオの1つだった。

繰り返しになるが、たしかに統合テストはビジネス上のシナリオで想定される操作をテストでも再現する必要がある。ただし、このようなビジネス上必ず検証しておく必要があるシナリオがあるが検証が難しい場合は、なるべく実際の操作から離れないようにしつつもある程度セオリーを破る必要もあるのではないかという気がした。

統合テストとpackage privateどちらをとるか

第4章で見たように、退行に対する保護はテスト中に実行されるコードの量によって変わります。そのため、管理下にない依存とのコミュニケーションの流れの中で、モックに置き換えられるコンポーネントをアプリケーションの境界に近いものにすれば、テストを実施する際に経由されるクラスの数が増え、より強力な退行に対する保護を得られるようになります。

単体テストの考え方/使い方 プロジェクトの持続可能な成長を実現するための戦略』, Vladimir Khorikov, マイナビ出版, 2022-12-28, 須田智之 訳, p.313-314

とても大事な、原則と言ってもよさそうなルールだと感じたのだが、このルールに従うと1つだけ残念なことが起きた。

そもそもこの話が出てきたのは以下のようなクラスがあった場合どちらをモックすべきかという文脈だった。

class SlackWebApiClient(client: WSClient) {
  val WebApiBaseUri = new URI("https://slack.com/api")

  def postMessage(
    authorizationToken: String,
    channelName: String,
    message: String,
  )(implicit ec: ExecutionContext): Future[Unit] =
    client
      .url(WebApiBaseUri.resolve("chat.postMessage").toString)
      .withHttpHeaders(
        "Authorization" -> s"Bearer $authorizationToken",
        "Content-Type" -> "application/json",
      )
      .post(
        Json.obj(
          "channel" -> channelName,
          "text" -> message,
        )
      )
      .map { response =>
        // SlackWebAPIはレスポンスのJSONのokプロパティで成否を判定する
        Try((response.json \ "ok").as[Boolean]) match {
          case Success(true) =>
            ()
          case Success(false) =>
            throw new Exception("SlackWebAPIの実行に失敗しました。")
          case Failure(_) =>
            throw new Exception(
              "SlackWebAPIから想定していない形式のレスポンスを受け取りました。"
            )
        }
      }
}

case class BatchResult(taskName: String, customerName: String)

/**
 * 顧客に関する何らかのバッチ処理の結果を顧客のSlackチャンネルに
 * 通知する用途に特化したSlackWebApiClientのラッパー
 */
class BatchResultNotifier(client: SlackWebApiClient) {
  val AuthorizationToken = "foo"
  val Channel = "notification"

  def notifyComplete(result: BatchResult): Future[Unit] =
    client.postMessage(
      AuthorizationToken,
      Channel,
      s"${result.customerName} 様: ${result.taskName}が完了しました。"
    )
}

エラーハンドリングがガバガバだったり、認証用トークンやら通知先のチャンネル名やらがハードコーディングしてあるのはサンプルコードなので見逃してほしい。 この時、BatchResultNotifierを使うコードの統合テストではBatchResultNotifierをモックするのではなく、最終的に顧客のSlackに投稿を行うSlackWebApiClientをモックすべきだというのが本書の主張だ。

なお、実際に最終的にSlackへの投稿を行なっているのはWSClientじゃないかと思うかもしれないが、本書ではサードパーティ製のライブラリについては直接モックするのではなくアダプタとなるクラスを作成し、それをモックすることを推奨している。 実際、WSClientはBuilderパターンを採用しており、url()withHttpHeaders()などのメソッドが全て新しいWSClientインスタンスを返すようになっているため、モックしようとすると面倒なことこの上ない。

たしかに、SlackWebApiClientのメソッドに渡される引数を確認しておけば、SlackWebApiClientに変更が入りでもしない限りは顧客に最終的に送られるメッセージの内容が変わってしまう可能性は低く、強力な対抗への保護が手に入る。

ただ、気にする人もそこまでいないのかもしれないがこのルールはScalaの機能と少し折り合いが悪い点がある。 Scalaにはprivate[foo] class Bar {}というように書くことでfooパッケージの中でのみBarを公開するということができる。 私はこのようなアクセス制御はコード補完のサジェスト汚染を回避する意味合いでもなるべくちゃんとやっておきたいと考えている。 上のコードで言えば、SlackWebApiClientを全体に公開してしまうと、認証用のトークンをどう取得するかといったビジネス上あまり重要でない処理がinfrastructure層の外で書かれてしまう可能性がある。 なので、当然SlackWebApiClientはinfrastructure層に該当するパッケージの中でのみ公開し、外部からはBatchResultNotifierのような用途ごとのラッパーを作らせてそちらを使わせたかった。

すると何が起こるか。 統合テストが書かれるパッケージからはSlackWebApiClientが参照できなくなるのでモックできなくなるのである。 SlackWebApiClientインターフェイスを用意しそちらを公開すればよいのではないかと思うかもしれないが、PlayFrameworkを使った開発でGuiceを使っている場合はインターフェイスさえあれば実質SlackWebApiClientの機能が呼び出せてしまうので何の解決にもならない。 結局、品質の高いテストを書くことを優先し、SlackWebApiClientはpublicにしたが、何かうまいやり方は無いかとモヤモヤしてしまった。

感想

単体テストの考え方/使い方」というタイトルに反し、優れた統合テストを書くための指針についても書いてくれているありがたいパートだった。 特にモックについてはこれまで「テストが速く終わる方がいいんだ!」と無闇やたらにRepositoryをモックにしていたと思えば、でも今のRepositoryの設計がイケていないのでいつか作り直す時のためにモックではなくDBを使いたいと考え始めたり、使い方に明確な指針を持てていなかったのでとても参考になった。

10章は、DBを使った統合テストを行うためにはスキーマVCSで管理できるようにしておくだとか、開発者ごとに個別のDBインスタンスを用意するだとか、マイグレーションツールを使っているのであれば自ずと達成されているであろう内容も多かったが、むしろRDB以外を使った統合テストを書かなければならない時にこういったことを考える必要があるのかもしれないと思った。

「単体テストの考え方/使い方」第2部を読んで考えたこと

前回

p-kino.hatenablog.com

レガシーコードのテストピラミッドの歪み

つまり、E2Eテストが持つテスト・ケースをもっとも少なくし、統合テストが持つテスト・ケースをE2Eテストより多くしながらも、単体テストよりも少なくし、単体テストが持つテスト・ケースをもっとも多くする、ということです。

単体テストの考え方/使い方 プロジェクトの持続可能な成長を実現するための戦略』, Vladimir Khorikov, マイナビ出版, 2022-12-28, 須田智之 訳, p.126

これは、私が現在携わっているようなレガシーコードの多いプロジェクトではそう上手く行かないように思われる。 レガシーコードは単体テストを書こうにも、「サービスクラス」と呼ばれている何かがたくさんの依存関係と複雑なロジックの両方を扱い、本書でいうところの「過度に複雑なコード」に陥っていることが多い。 つまり本書で単体テストの対象とされている「ドメインモデル」が存在しないし、実際に単体テストを書こうとしてもプロセス外依存とのinfrastructure層に適切に隠蔽されていないので、実行時間が短い、各テストケースが隔離されているという条件を達成するのはまず不可能になる。 単体テストを書こうと思ったらまずは「過度に複雑なコード」を複雑なロジックを扱うドメインモデルと、多くの依存の間をとりもつコントローラに分離する必要があるのだが、テスト対象のメソッドが呼び出すコードが合計で数百行あったり、依存しているクラスも全くテストされていなかったりするといきなり手を加えるというのも少々こわい。 結果として最初に統合テストを1つ用意し、あとはそれを頼りにリファクタしていくというやり方になりがち。

ただ、統合テストはやはりセットアップのコードが長くなりがちだし、実行に時間がかかり迅速なフィードバックが得られないのがきついので、正常なテストピラミッドに近づけていきたいという思いもある。 この書籍は質のいい単体テストを書くにはまずコードがまともなアーキテクチャに則って書かれている必要があると訴えているように感じ、レガシーコードの多いプロジェクトの扱いはスコープ外なのだろうが、いつかそういう場合にどうやってテストピラミッドの歪みを直すかについても知りたいと思った。

関数型アーキテクチャと現実との折り合い

今回見てきた訪問者記録システムは関数型アーキテクチャをうまく導入することができたのですが、その理由は、関数的核(functional core)を呼び出す前に、すべての入力値を集めることができたからです。しかしながら、実際のシステムでは、決定を下す流れがこの訪問者記録システムほど単純ではないことがよくあります。たとえば、決定を下している最中に、途中で得た結果を使って新たにプロセス外依存からデータを取得しなければならないような場合です。

単体テストの考え方/使い方 プロジェクトの持続可能な成長を実現するための戦略』, Vladimir Khorikov, マイナビ出版, 2022-12-28, 須田智之 訳, p.206

私としてはせっかく業務でScalaを使っているので可能な限り関数型アーキテクチャで行きたいと思ったが、実際、ちょうどその時業務で実装していた機能で

  1. 他社の外部システム(Aとする)からデータを取得する
  2. Aから取得したデータにビジネスロジックから考えて不正な部分が無いかバリデーションを行う
  3. Aから取得したデータに、自社DBの特定のテーブルのキーとなる値が含まれているので、それを元に結合する
  4. ただし、自社DBにキーに対応するレコードがまだ登録されていないことがあり、その場合見つからなかったデータをまた別の外部システム(Bとする)から取得する
  5. 外部システムからキーに対応するデータがBから見つかれば、それを自社DBに登録し、Aから取得したデータと結合する
  6. Aから取得した全てのデータに対し自社DBのレコードが紐づけられたら後続の処理を行う

という処理を実装する必要があり、関数型アーキテクチャと現実の乖離を実感した。 この場合、処理を行うためには

  • Aからのデータ
  • 自社DBのデータ
  • Bからのデータ

が必要になり、最初にそれらを全て取得してしまうこともできないわけではないが、個人的には2のバリデーションに引っかかるようなおかしなデータをAから受け取った場合は、自社DBやBへのアクセスは無駄でしかないので行いたくない。 となると、本書にあるとおりどうしても可変殻(純粋でない処理を行う部分)→関数的核(純粋な計算のみ行う部分)→可変殻→関数的核→...というように、純粋な計算と純粋でない処理が入り混じったフローにならざるを得ない。

本書では処理をDBや外部APIなどのプロセス外部に依存する部分と、プロセス外部に依存しないビジネスロジックの部分に分け、プロセス外部に依存する部分でどのような処理を行うかという判定をなるべくビジネスロジックの部分でやるというアプローチをとっていた。 私も、アーキテクチャ銀の弾丸は無いのだろうしそれが妥当な落とし所ではないかと思うのだが、一方でそれでも関数型アーキテクチャを実践する人はこの問題にどう立ち向かっているのかということも気になってきた。 この問題については「Domain Modeling Made Functional」, 「Functional and Reactive Domain Modeling」という書籍を読むのがよさそうだったので、いつか手にとってみようと思う。

感想

7章が単体テストに留まらない設計の話も多く、アーキテクチャには銀の弾丸が無いので、本当にそれでいいのか?と悩みながら読み進めることが多かった。

たとえば、本書では以下のようなコードがリファクタ後のコードとして紹介されている。

public class User
{
  public int UserId { get; private set; }
  public string Email { get; private set; }
  public UserType Type { get; private set; }

  public void ChangeEmail(string, newEmail, Company company)
  {
    if (Email == newEmail)
        return;

    UserType newType = company.IsEmailCorporate(newEmail)
            ? UserType.Employee
            : UserType.Customer;

    if (Type != newType)
    {
      int delta = newType == UserType.Employee ? 1 : -1;
      company.ChangeNumberOfEmployees(delta);
    }

    Email = newEmail;
    Type = newType;
  }
}

// 『単体テストの考え方/使い方 プロジェクトの持続可能な成長を実現するための戦略』, Vladimir Khorikov, マイナビ出版, 2022-12-28, 須田智之 訳, p.234

コードの背景をざっくり説明すると

  • ChangeEmailが受け取ったnewEmailドメインがcompanyのものであれば、UserUserTypeUserType.Employeeにし、そうでなければUserType.Customerにする
  • UserTypeの変更があった場合はcompanyが持つ従業員数プロパティを増減させる(UserTypeUserType.Employeeに変更されたら1増やし、UserType.Customerに変更されたら1減らす)

というものだ。

私が気になるのはやはり、この方法ではChangeNumberOfEmployeesというメソッドが用意されていることからもわかるようにCompanyを可変にしなければならないということだ。 ただ、Companyを不変にするとChangeEmailが新しいCompanyを返す必要があり、それはそれで不自然な気もする。

また、本書で紹介されているドメインイベントを用いた副作用のハンドリングについても、Userドメインイベントをどんどん追加していくための可変なコレクションをプロパティとして持たなければならないという問題がある。 とはいえこちらはドメインイベントを保持するコレクションを不変にしても、Userのメソッドが新しい状態のUserを返すという動作になり、あまり不自然さは感じないか。

しかし、あまり不変にこだわると今度は関数型アーキテクチャが抱えるパフォーマンスの問題に行き着いてしまう。

ただ、テストの観点から言えばどのようなアーキテクチャを選ぼうとも以下の2点が大事になるのではないかという気がした。

  • ビジネスロジックが書かれるクラスでDBや外部APIファイルシステムなどのプロセス外依存を扱わないようにし、単体テストが書けるようにしておく
  • リポジトリや外部APIのクライアントのクラスへの依存をたくさん持ち、なおかつ複雑なロジックが書かれているクラスはテストしないでいると危険だがテストが書きづらいクラスなので、そういうクラスが生まれないよう、複雑なロジックが書かれるドメインモデルと、ドメインモデルとリポジトリなどのプロセス外依存を扱うクラスの連携を指揮するコントローラに分離し、ドメインモデルは単体テストを、コントローラは統合テストを書く

関数型アーキテクチャドメインイベントについてはまだまだ前提知識が足りていないので、より理解を深めてScalaに合うアーキテクチャを探っていきたい。

「単体テストの考え方/使い方」第1部を読んで考えたこと

最近テストコードの保守が足を引っ張ってしまっている気がしたので、一度テストについて学習する機会をとるべきだと思い、「単体テストの考え方/使い方」を読んでいる。 主に書く言語がRubyからScalaに変わり、テストではなく型で保証させるべき事柄も出てきたのでテストとの向き合い方を変える必要性を感じていたというのもある。 まだ途中だが、テストに関してこれまでぼんやりと感じていたことが理論的に整理され、とてもよい本だと感じる。

ドメイン層以外に対するテスト

つまり、重要なのは、単体テストにかける労力をシステムにとって非常に重要な部分に向ける、ということなのです。一方、そこまで重要ではない部分に対する検証は簡易に、もしくは、間接的に行います。ほとんどのアプリケーションにおいて核となる部分はビジネス・ロジックを含む部分、つまり、ドメイン・モデルになります。

一方、ドメインモデルではないコードには、次の3つのコードがあります:

  • インフラに関するコード
  • 外部サービスや依存関係にあるもの(たとえば、データベースやサード・パーティのシステム)
  • 構成要素同士を結び付けるコード

単体テストの考え方/使い方 プロジェクトの持続可能な成長を実現するための戦略』, Vladimir Khorikov, マイナビ出版, 2022-12-28, 須田智之 訳, p.48-49

これらの記述では、プレゼンテーション層やユースケース層のコードに対するテストはどうするべきなのか読み取れなかった。 もしかすると「構成要素同士を結び付けるコード」というのがユースケース層のコードを指しているのかもしれない。

ただ私の考えとしては、ドメインモデルはドメインを表現したものに過ぎず、それ単体ではドメインにもともと存在する課題を何一つ解決しない。 ユースケース層でドメインモデルを使うことで初めてドメインに存在している課題を解決できるので、ユースケース層のコードもまた価値があるのではないかと思う。 「これはドメインに存在するロジックではなく、アプリケーションのロジックだな」と思ったものはユースケース層に書くことも多いので、私はユースケース層についても単体テストを書いていこうと思った。

一方でプレゼンテーション層はアプリケーションにとって重要な部分とは言い難く、複雑なロジックが入り込むこともあまり無いので、統合テストでカバーすれば問題無いように思う。

(2024-01-27追記)この本を読み進めたところ、どうもユースケース層も単体テストはしない、というよりする必要がないほどシンプルに作るべきということだった。 以下の文は、たとえばビジネスロジック、プロセス外依存とのやりとり、ユーザーからのリクエストのハンドリングが全部書かれているControllerのようなクラスに対し、どのように質の高い単体テストを書いていくかということを述べた文である。

多くの場合、テストをすることが難しくなるのはテスト対象のコードがフレームワークとなる依存に直接結び付く場合です。そのような依存には、たとえば、非同期や複数スレッドでの実行、ユーザ・インターフェイス、プロセス外依存とのコミュニケーションなどがあります。

このような依存と結び付いてしまったロジックをテストするためには、その過度に複雑なコードからテストを行いやすい部分を抽出しなくてはなりません。そして、その抽出された部分を包み込む質素(humble)なクラスを作成し、その作成した質素なクラスに対してテストをすることが難しい依存を結び付けるようにします。このとき、その質素なクラスをテストする必要がないようにします。

単体テストの考え方/使い方 プロジェクトの持続可能な成長を実現するための戦略』, Vladimir Khorikov, マイナビ出版, 2022-12-28, 須田智之 訳, p.219-220

これを読む限りでは、ユースケース層に対する単体テストは行わず、統合テストやプレゼンテーション層も通したE2Eテストでカバーするのがよさそう。

私のテストの問題点と今後どうしていくか

この本を読んでいて、今まで私は何をモックするかという基準が曖昧だったので、古典学派としてもロンドン学派としても中途半端なスタイルでテストを書いてしまい、両者の悪いとこどりになってしまっていたのではないかと思った。 その結果、リファクタリングや多少の設計の変更ですぐ壊れ、その割にテストが失敗した時にどのクラスが問題で落ちているのかよくわからないということが起きてしまっていた。

では今後、古典学派とロンドン学派のどちらの立場に立つのかという話になってくるが、少なくとも今の仕事を続けている内は古典学派の立場に立つのがよさそうだと思った。 ロンドン学派のメリットに対する筆者の反論の一部に賛同したのと、現在業務で主にPlayFrameworkの仕組み上、ロンドン学派のメリットが薄れるからだ。

単体テストでは、1単位のコード(a unit of code)を検証するのではなく、1単位の振る舞(a unit of behavior)を検証するようにします。つまり、問題領域において意味があるもの(理想としては、ビジネス・サイドの人たちが有用であると考える何か)を検証するようにします。

単体テストの考え方/使い方 プロジェクトの持続可能な成長を実現するための戦略』, Vladimir Khorikov, マイナビ出版, 2022-12-28, 須田智之 訳, p.47

少なくとも、1単位の振る舞いが検証されてさえいれば、それは良い単体テストとなるのです。逆に、検証の対象となるものがそのことを満たせていなければ、その単体テストが何を検証しようとしているのかが曖昧になるため、質の悪い単体テストが作成されることになります。単体テストにおいて、各テスト・ケースがすべきことは、そのテストに関わる人たちにテスト対象のコードが解決しようとしている物語(story)を伝えることなのです。そして、その物語を伝えるためには凝集度(cohesion)を高め、非開発者でも理解できるようにすることが必要となるのです。

単体テストの考え方/使い方 プロジェクトの持続可能な成長を実現するための戦略』, Vladimir Khorikov, マイナビ出版, 2022-12-28, 須田智之 訳, p.47-48

これは本当にそのとおりだと思っていて、「このテストコードを読んだ人はこのクラスが何のためのクラスなのかわかるのだろうか?」と悩むことが今まで多かった。 私は今の仕事でレガシーコードにテストを追加するタスクも暇を見つけて行なっているのだが、対象のコードがビジネス上でどんな役割を果たすのかがわかるとコードの流れも多少見えて捗る。 レガシーコードについては対象のコードが果たす役割は誰かに聞くか不可能であれば自分でコードを読んで推測するしか無いのだが、それをテストに語らせることができれば後任の開発体験は確実によくなるだろうと思う。 今後は単体テストにはなるべく解決しようとしている問題を語らせること、それができない時はテスト対象のコードの凝集度が低くなっている可能性を疑ってみようと思う。

また、書籍ではロンドン学派のもたらすメリットの1つに以下のようなものが挙げられている。

協力者オブジェクトをモックに置き換えることはテスト対象となるクラスの検証を簡単にします。特に、テスト対象のクラスが必要とする依存が多く、それらの依存がさらに別の依存を必要とするような複数層の複雑な依存関係を築いている場合、テスト対象のクラスが直接アクセスする依存をテスト・ダブルに置き換えることで、この複雑な依存関係を断ち切れるようになります。

単体テストの考え方/使い方 プロジェクトの持続可能な成長を実現するための戦略』, Vladimir Khorikov, マイナビ出版, 2022-12-28, 須田智之 訳, p.48

この点はDIコンテナをデフォルトで採用するPlayFrameworkでは完全に無視できる。DIコンテナの仕組みに乗っかっている限りは、たとえ100個のクラスに依存しているクラスだろうがテストコード中では1行でインスタンスを取得できる。

一方でこのロンドン学派のもたらすメリットに対する筆者の反論は、PlayFrameworkでの開発においても気をつけなければならないと感じた。

しかしながら、この主張は間違った問題に目を向けています。と言うのも、本来考えるべきことは膨大で複雑な依存関係を持つクラスを検証するための方法を見つけ出すことではなく、そのような複雑な依存関係を構築しなくても済むようにするための方法だからです。

単体テストの考え方/使い方 プロジェクトの持続可能な成長を実現するための戦略』, Vladimir Khorikov, マイナビ出版, 2022-12-28, 須田智之 訳, p.48-49

依存関係がやたらに多いクラスは、公開メソッドがやたら多く、それぞれのメソッドで依存関係を使ったり使っていなかったりということがよくある。そういうクラスは凝集度が低くなってしまっている可能性があり、DIコンテナに頼っているとそれを見落としそうなので気をつけなければと思った。

テストケースの隔離の実現方針

一方、古典学派は、以前に述べたように隔離についてロンドン学派とは異なる解釈をしています。その違いとは、古典学派では、単体テストにおいて隔離する必要があるのはコードではなくテスト・ケースであり、各テスト・ケースをお互いに影響を与えることなく個別に実行できるようにしなくてはならい、と考えていることです。

単体テストの考え方/使い方 プロジェクトの持続可能な成長を実現するための戦略』, Vladimir Khorikov, マイナビ出版, 2022-12-28, 須田智之 訳, p.37

これはPlayFramework + ScalaTestでは相当難しい気がする。 PlayFrameworkのテストではGuiceOneAppPerSuiteを使うとappという不変なインスタンス変数にApplicationのインスタンスを格納し、テストスイートを通して1つのApplicationのインスタンスを使い回す。 モックを使っているApplicationのインスタンスがテストスイートを通して使いまわされるわけだから、テストケースを並列実行した場合、1つのモックに対し並列で呼び出しが行われ、テストケース間で影響を与え合ってしまう。

ではテストケースごとにApplicationのインスタンスを作り直すGuiceOneAppPerTestはどうかというとこちらはインスタンス変数appが可変になっており、テストケースの開始の度にApplicationのインスタンスを作り直してappに格納し直すという仕組みになっているようなので、やはりテストケース間で影響を与え合ってしまう。

もちろん自作関数を用意すればこの問題は解決するのだが、以前それで罠にハマったこともあり、そこまでやるのか?という気もする。 自分としてはテストスイートのクラス1つ1つが互いに影響を与え合わないようにし、それぞれのテストスイートは並列に実行できるようにしておくところまでで妥協しておくのがよいように思われる。 この場合はモックにすべき共有依存があればGuiceOneAppPerTestを、無いならGuiceOneAppPerSuiteを使っていくのがよさそう。

感想

単体テストを保守する工数にはRubyで開発を行なっていた頃から悩まされていたが、それでも退行が怖くてコードが変更不能になるよりは...と思いテストを書き続けてきた。 なので、単体テストを書く本当の目的は退行に対する保護それ自体ではなくプロジェクトの成長を持続可能なものにすることにあり、質の悪いテストを残すとその妨げになってしまうという考えに衝撃を受けたと同時に、うなずくしかなかった。 今までテストコードはおまけ程度に考え、多少ダーティに感じる書き方も許容していたが、今後はプロダクションコードと同等に大事なものであるという意識で向かっていきたい。

AkkaのClassic ActorにGuiceを使ってDIする方法

Classic Actorを今時使っているところはあまり無いと思うが、現在携わっているプロジェクトでは既に多くのActorがClassicで書かれてしまっており、中々完全に移行できない。 このプロジェクトはPlay Frameworkで実装されているWebアプリでバッチ処理の定期実行にakka-quartz-schedulerを使っているため、ActorへのDIもなるべくGuiceでやりたい。 しかしこれまで開発していたメンバーがそのやり方がわからなかったのか、普通のコンストラクタインジェクションでDIが行われていた。 ただでさえActorは親子関係を持つことがほとんどであり、単なるコンストラクタインジェクションでは親から子にひたすら依存関係をバケツリレーしていくことになるので、新たな依存関係が増えると祖先のActorのコンストラクタを全て修正することになり、とてもしんどいので、方法を探すことにした。

結論としては、Classic Actor自体ではなく、Classic Actorを生成する際に必要になるPropsを生成するFactoryクラスを用意し、そのFactoryにDIすればよかった。

class ParentActor(childPropsFactory) extends Actor {
  override def receive: Receive = {
    case "Some Message" => context.actorOf(childPropsFactory.create)
  }
}

class ParentActorPropsFactory @Inject()(
  childPropsFactory: ChildPropsFactory,
) {
  def create: Props = Props(
    classOf[ParentActor],
    childPropsFactory,
  )
}

class ChildActor(foo: Foo, bar: Bar) extends Actor {
  override def receive: Receive = 
    case "Some Message" => ???
  }
}

class ChildActorPropsFactory @Inject()(
  foo: Foo,
  bar: Bar,
) {
  def create: Props = Props(
    classOf[ChildActor],
    foo,
    bar,
  )
}

これならば子孫で新たな依存関係が必要になっても、修正するのはそのActorのコンストラクタとPropsのFactoryの2つだけで済み、分かりやすい。

ただし、@Singletonアノテーションが付いたクラスには気をつける必要がある。 アクターモデルでは各Actorはなるべく何も共有しないことが望ましい。 しかし、ParentActorPropsFactoryやChildActorPropsFactoryを頂点とする依存関係のツリーに@Singletonが付いてるクラスが混じっていると、複数のActor間で1つのインスタンスを共有することになってしまう。

なお、Propsを生成するクラスを生成すること自体については、公式ドキュメントでPropsを生成するメソッドをActorのコンパニオンオブジェクトに持たせることが推奨されているので、それが別のクラスになったところで問題無いと思われる。

RubyからScalaに移る上で大事だったこと

前職ではRuby on Railsでの開発がメインだったが、転職してからはScala/Play Frameworkでの開発が主になり、それに伴い開発の手法, メンタルモデルも大きく変わった。 それに適応していくために特に大事だったと思うことを書く。

型駆動開発で進める

Scalaは記述量を減らすための工夫が至るところでされていると感じる。それでも動的型付け言語と比べると抽象を全て型として定義するため、定義する型の数からして違う。そのため、実装を終えてから出戻りが発生してしまうとものすごい時間が溶ける。

私はScalaでの開発を始めた当初、1つのクラスのテストや実装を一気通貫でやっていたが、作ったクラスを後で他のクラスと協調させる段階になって不都合が明らかになり、しばしば工数を無駄にしてしまった。 しかもそのような状況で焦って考え直したインターフェースはよくないものであることが多く、プロジェクトが一旦終わったあと、余裕のあるタイミングでまるごと作り直すことになった。

そのような事態を避けるために、型宣言だけ先にやってしまい、実装は後回しにするという手法が有効だった。

作為的な例だが"Scalaスケーラブルプログラミング第4版:4,600円"のような、<商品名>:<定価>という形式の文字列を渡すと、価格を消費税込みの金額に直した値を数値で返す関数の実装について考えてみる。 以下のような手順を踏めば目的の計算ができそう。

  • 与えられた文字列から価格の部分の文字列を抽出する
  • 価格の文字列を数値に直す
  • 税額を足す

まず、これらをそれぞれ関数として宣言する。

def extractPrice(s: String): Option[String] = ???

def toInt(price: String): Option[Int] = ???

def addTax(price: Int): Int = ???

ここでそれぞれの関数の実装には入らず、先にこれら3つの関数を組み立てる。 こうすることで、自分が考えているロジックが型レベルで矛盾していないか予め確認できる。

def extractPriceIncludingTax(s: String): Option[Int] =
  for {
    priceStr <- extractPrice(s)
    price <- toInt(priceStr)
  } yield addTax(price)

今回は実装を省くが、この後ようやくそれぞれの関数を実装する。 実際の開発ではこの関数1つ1つがクラスのpublicメソッドになるイメージ。

当然、型の宣言を終えて実装していく内にやはりこのシグニチャではダメだったということも出てくるが、次に書くレイヤードアーキテクチャを意識し始めてからは、フレームワークやライブラリに由来するデータ型をアプリケーションロジックやビジネスロジック中でなるべく登場させないように心がけるようになったため、それも少なくなってきたように思う。

戦術的設計からDDDに入門する

これは、私がある程度複雑なドメインを持つ業界のアプリケーションを開発しているためポジショントークかもしれないが、DDDの戦術的設計について学習し実践していくことは、Scalaでの開発を始めてから行なったことの中でもコスパがよかった。

既にたくさんの人に言われているとおり、戦術的設計はドメイン駆動設計の全てではないし、私が実践しているのは軽量DDDと呼ばれるものだろう。 しかし、以下のようなものが無秩序にいくつかのクラスに押し込められた様を想像してほしい。

はっきり言ってドメイン知識をコードに落とし込むどころではない。 それどころか、そもそもテストが書けず変更ができないコードになる可能性も高い。 戦略的設計、更にはより深いDDDの哲学の実践に進む前に、テスタブルで変更に耐えられるコードにする必要がある。

戦術的設計を実践することで自ずとテスタブルなコードに近付いていくことが多いし、変更があった場合も大体あの辺を修正すればいいのではないかと見当がつきやすくなった。 私にからすれば軽量DDDはアンチパターンというより、最悪の状態からDDDの実践へ至る途中にある過程の1つだと思う。 プロトタイプの開発では軽量DDDの実装コストがメリットを上回ってしまうかもしれないが、長期的に運用され変更も入るソフトウェアなら、DDDを実践しきれていなくてもコードが整備されていた方がよほどいい。

DDDの足がかりとしては、ドメイン駆動設計 モデリング/実装ガイドに大いに助けられた。 あまり時間がなくても1週間ほどでさっと読めるし、それでいて「このクラスはどのレイヤーに置くべきか?」という疑問をいくらか自分で考えるための軸を構築してくれるので、強く勧めたい。

テストコードを減らしていく

これは私がまだ取り組んでいないので、「RubyからScalaに移る上で大事だったこと」と言い切れないのだが、重要ではないかと思っている。

Rubyで開発していた頃はまだRBSRuby本体にバンドルされるようになってそう時間も経っておらず、私が携わっていたプロジェクトでも導入していなかったので型検査が無かった。 なので、依存関係のツリーの途中にあるクラスが設計の見直しによりインターフェースを変更されるも、依存している側の修正が十分ではなくNoMethodErrorを招いてしまう可能性が考えられ、それに対応するためにある程度E2Eに近いテストコードを書いておかないと安心できなかった。

しかし、型検査がある場合はそのようなことは無くなる。これは一例に過ぎないが、型検査を導入したことによって、テストとの向き合い方も変えていき、型のもたらすメリットを更に享受していく必要があるように感じる。

さしあたっては単体テストの考え方/使い方という書籍が参考になるのではないかと思い、近々読んでみようと思っている。