テストクラスを継承するとインターフェイスのテストが捗る
User
オブジェクトの永続化を行う UserRepository
というインターフェイスがあるとします。
また、それを実装した2つのクラス InMemoryUserRepository
と MySqlUserRepository
があるとします。
これらのクラスのテストはどのように書けばよいでしょうか?
(テストフレームワークは JUnit5 です)
普通に考えると InMemoryUserRepository
と MySqlUserRepository
の2つのテストを個別に書かないといけないように思います。
しかし、これらのクラスはどちらも UserRepository
として振る舞うはずなので、UserRepository
のインターフェイスのみを使ってテストを書けば1回テストを書くだけで2つの具象クラス両方をテストできます。
例
UserRepository
が以下のようなインターフェイスだったとします。
(パスワードをそのままデータベースに保存するのはアウトですが、例なので許してください)
interface UserRepository { // 指定した id を持つユーザーを取得する。 // 存在しないときは null を返す。 fun getUser(id: UserId): User? // 名前とパスワードを指定して新しくユーザーを作成する。 fun createUser(userName: UserName, password: Password): User }
実装として、InMemoryUserRepository
と MySqlUserRepository
があります。
class InMemoryUserRepository : UserRepository { private var nextUserId = 1L private val users = HashMap<UserId, User>() override fun getUser(id: UserId) = users[id] override fun createUser(userName: UserName, password: Password): User { val id = UserId(nextUserId) nextUserId += 1 val user = User(id, userName, password) users[id] = user return user } }
class MySqlUserRepository( private val dataSource: DataSource ) : UserRepository { override fun getUser(id: UserId): User? { // 略 } override fun createUser(userName: UserName, password: Password): User { // 略 } }
まずは、インターフェイスである UserRepository
に対してテストを書きます。
テストクラスは abstract class にしておき、テスト対象である UserRepository
は abstract val として宣言します。
internal abstract class UserRepositoryTest { abstract val sut: UserRepository @Test @DisplayName("getUser - 正常系") fun getUser() { // Setup sut.createUser(UserName("alice"), Password("open sesame")) // Exercise val user = sut.getUser(UserId(1)) // Verify val expected = User(UserId(1), UserName("alice"), Password("open sesame")) assertThat(user).isEqualTo(expected) } @Test @DisplayName("getUser - 存在しないユーザーを取得すると null が返る") fun getNonexistentUser() { // Exercise val user = sut.getUser(UserId(100)) // Verify assertThat(user).isNull() } @Test @DisplayName("createUser - 正常系") fun createUser() { // Exercise val user = sut.createUser(UserName("bob"), Password("secret")) // Verify val actual = sut.getUser(user.id) val expected = User(user.id, UserName("bob"), Password("secret")) assertThat(actual).isEqualTo(expected) } }
これでテストケースの定義が書けました。では、このクラスを継承してテストの実体を作ります。
まずは InMemoryUserRepositoryTest
から。
internal class InMemoryUserRepositoryTest : UserRepositoryTest() { override val sut: UserRepository = InMemoryUserRepository() }
たった3行でした。
次は、MySqlUserRepositoryTest
を書きます。
internal class MySqlUserRepositoryTest : UserRepositoryTest() { private val dataSource = /* テスト用 MySQL の DataSource を作成する処理 */ override val sut: UserRepository = MySqlUserRepository(dataSource) init { val flyway = Flyway.configure() .dataSource(dataSource) .load() flyway.clean() flyway.migrate() } }
MySQL を使う場合は DataSource を作ったり、テストケースごとにデータベースを初期化したりする必要があるので、InMemory よりもちょっとコードが増えています。 この例では Flyway を使ってデータベースの初期化を行っています。
この状態でテストを実行すると InMemory と MySQL それぞれに対して UserRepositoryTest
で定義した3つのテストケースが実行されます。
2つの具象クラスに対するテストを共通化できたというわけです。
以上、テストクラスを継承することでインターフェイスのテストを行う例でした。