前置き
これらの記事で mix-in injection 1 を導入し、プロダクション用インスタンスの構築方法を解説しましたが、テスト用インスタンスの構築については言及がありませんでした。所詮テストだしどう書いてもいいのですが、短く綺麗に書くための tips も紹介します。このパターンはいろんな人によりだんだん洗練されていったもので、まだ改善の余地があるかもしれません。
ここでは Scala + Scalatest + Mockito の利用を想定します。別の言語・フレームワークでも使い回せる部分があるかもしれないし、ないかもしれません。
テストの書き方
まずは簡単に実装例を示します。
case class User(id: Int, name: String)
class DatabaseException(message: String = null, cause: Throwable = null)
extends RuntimeException(message, cause)
trait UserRepository {
/**
* ユーザーを新規作成して保存します。ID は自動的に付与されます。
* @throws DatabaseException DBエラーの場合
*/
def insert(name: String): User = ???
}
trait UsesUserRepository {
def userRepository: UserRepository
}
trait UserService extends UsesUserRepository {
/**
* ユーザーを作成します。
*/
def createUser(name: User): Future[CreateResult] = {
Future(userRepository.insert(name)).map(user => Created(user)).recover {
case NonFatal(e) =>
e.printStackTrace()
DBError
}
}
}
object UserService {
sealed trait CreateResult
case class Created(user: User) extends CreateResult
case object DBError extends CreateResult
}
class UserServiceSpec extends WordSpec {
trait Setup {
self =>
val user = User(1, "name")
// 依存先のモックの作成
val userRepository = Mockito.mock(classOf[UserRepository])
Mockito.when(userRepository.insert(Matchers.anyString))
.thenReturn(user)
// テスト対象の作成とモックの注入
val userService = new UserService {
val userRepository = self.userRepository
}
}
"createUser" should {
def wait[T](f: Future[T]): T = Await.result(f, Duration.Inf)
"successfully create a user" in new Setup {
val result = wait(userService.createUser(user.name))
assert(result === Created(user))
Mockito.verify(userService).createUser(user.name)
}
"return DBError if fails" in new Setup {
Mockito.when(userRepository.createUser(Matchers.any))
.thenThrow(new DatabaseException)
val result = wait(userService.createUser(user.name))
assert(result === DBError)
}
}
}
テストコードの上から順に見所を解説していきます。
Setup トレイト
trait Setup {
...
}
"successfully create a user" in new Setup {
...
}
テストケースごとにインスタンスを作るのは面倒臭いものです。Setup トレイトを作り、テストケースの in
の後で new
すると楽になります。テストケースごとに新しくリポジトリのモックやサービスを構築し、あたかもローカルスコープに放り込んだかのように記述できます。
self エイリアス
self =>
val userService = new UserService {
val userRepository = self.userRepository
}
val userRepository = userRepository
とすると自身を参照してしまいます。モックを userRepository_
などと宣言するよりはこちらの方が綺麗です。
クロージャを使い慣れている人にとっては常識的なテクニックですが、私の周辺で導入されたのは比較的後の時代になってからでした。
モックのデフォルトの挙動
Mockito.when(userRepository.insert(Matchers.anyString))
.thenReturn(user)
依存先のモックを作る場合、あらかじめ正常系の挙動を定義しておきます。Mockito のモックは when
節を複数回定義すると後のものが勝つので、異常系のテストケースでは異常動作をしてほしいメソッドだけ挙動を指定すればよいです。依存性が増えても、テストケースを小さく保つことができるようになります。
締め
DI が必要とされるようなサービス開発などの場面では、ロジックが比較的 2 単純なので、開発速度は大抵短く書けるかどうかとタイピング速度に依存します。テストを短く書く方法を極めて、さくさくサービスを作っていきたいものです。