37
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Mix-in injection における最強のテスト用インスタンス構築パターン

Posted at

前置き

これらの記事で mix-in injection 1 を導入し、プロダクション用インスタンスの構築方法を解説しましたが、テスト用インスタンスの構築については言及がありませんでした。所詮テストだしどう書いてもいいのですが、短く綺麗に書くための tips も紹介します。このパターンはいろんな人によりだんだん洗練されていったもので、まだ改善の余地があるかもしれません。

ここでは Scala + Scalatest + Mockito の利用を想定します。別の言語・フレームワークでも使い回せる部分があるかもしれないし、ないかもしれません。

テストの書き方

まずは簡単に実装例を示します。

UserService.scala
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
}
UserServiceTest.scala

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 単純なので、開発速度は大抵短く書けるかどうかとタイピング速度に依存します。テストを短く書く方法を極めて、さくさくサービスを作っていきたいものです。

  1. Minimal Cake Pattern という単語を使ってきましたが、mix-in injection のほうがしっくり来る気がしてきたので個人的にはこちらを使っていきたい

  2. 汎用的なライブラリとか、アルゴリズム・データ構造の実装に比べて

37
36
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?