こんにちは、Habr!良いコードを書くだけでは十分ではありません。それでも、適切なユニットテストでカバーする必要があります。前回の記事では、簡単なWebサーバーを作成しました。ここで、いくつのテストを書いてみます。定期的、プロパティベース、モック。詳しくは猫の下でようこそ。
コンテンツ
- スカラの学習:パート1-スネークゲーム
- Scalaの学習:パート2-画像をアップロードしたTodoシート
- Scalaの学習:パート3-ユニットテスト
リンク
Sources
Images docker image
したがって、ユニットテストには3つのライブラリが必要です。
- テストを作成するためのライブラリ
- テストデータを生成するライブラリ
- オブジェクトをモックするライブラリ
ScalaTest ライブラリを使用してテストを作成しました
"org.scalatest" %% "scalatest" % "3.2.0" % Test
ScalaCheck を使用して、プロパティベースのテスト用のテストデータを生成しました。
"org.scalacheck" %% "scalacheck" % "1.14.3" % Test
その延長コンバインScalaTest + ScalaCheck ScalaTestPlusScalaCheck
"org.scalatestplus" %% "scalacheck-1-14" % "3.2.0.0" % Test
ScalaMock を使用してオブジェクトをモックしました
"org.scalamock" %% "scalamock" % "4.4.0" % Test
満たされた(空ではない)文字列のタイプを表す単純なクラス。それをテストします。
package domain.common
sealed abstract case class FilledStr private(value: String) {
def copy(): FilledStr = new FilledStr(this.value) {}
}
object FilledStr {
def apply(value: String): Option[FilledStr] = {
val trimmed = value.trim
if (trimmed.nonEmpty) {
Some(new FilledStr(trimmed) {})
} else {
None
}
}
}
テスト用のクラスを作成する
class FilledStrTests extends AnyFlatSpec with should.Matchers with ScalaCheckPropertyChecks {
}
同じ行からクラスを作成するときに、同じデータを受け取ることを確認するメソッドを作成します。
"equals" should "return true fro equal value" in {
val str = "1234AB"
val a = FilledStr(str).get
val b = FilledStr(str).get
b.equals(a) should be(true)
}
最後のテストでは、手動で作成した文字列に隠しました。次に、生成されたデータを使用してテストを実行しましょう。関数のプロパティをテストするプロパティベースのアプローチを使用して、そのような入力データを使用して、そのような出力データを受信します。
"constructor" should "save expected value" in {
forAll { s: String =>
// . .
whenever(s.trim.nonEmpty) {
val a = FilledStr(s).get
a.value should be(s)
}
}
}
必要なデータのみを使用するようにテストデータジェネレータを明示的に構成できます。たとえば、次のようになります。
//
val evenInts = for (n <- Gen.choose(-1000, 1000)) yield 2 * n
//
forAll (evenInts) { (n) => n % 2 should equal (0) }
また、ジェネレーターを明示的に渡すことはできませんが、ジェネレーターとしてテストに自動的に渡されるように、任意にその暗黙を定義します。たとえば、次のようになります。
implicit lazy val myCharArbitrary = Arbitrary(Gen.oneOf('A', 'E', 'I', 'O', 'U'))
val validChars: Seq[Char] = List('X')
// Arbitrary[Char] .
forAll { c: Char => validChars.contains(c) }
Arbitraryを使用して複雑なオブジェクトを生成することもできます。
case class Foo(intValue: Int, charValue: Char)
val fooGen = for {
intValue <- Gen.posNum[Int]
charValue <- Gen.alphaChar
} yield Foo(intValue, charValue)
implicit lazy val myFooArbitrary = Arbitrary(fooGen)
forAll { foo: Foo => (foo.intValue < 0) == && !foo.charValue.isDigit }
それでは、もっと真剣にテストを書いてみましょう。TodosServiceの依存関係をモックします。2つのリポジトリを使用し、リポジトリはUnitOfWorkトランザクションの抽象化を使用します。最も簡単な方法をテストしてみましょう。
def getAll(): F[List[Todo]] =
repo.getAll().commit()
リポジトリを呼び出すだけで、リポジトリ内のトランザクションを開始してTodoリストを読み取り、リポジトリを終了して結果を返します。また、テストでは、F [_]の代わりにIdモナドが配置され、そこに格納されている値が返されます。
class TodoServiceTests extends AnyFlatSpec with MockFactory with should.Matchers {
"geAll" should " " in {
// .
implicit val tr = mock[TodosRepositoryContract[Id, Id]]
implicit val ir = mock[InstantsRepositoryContract[Id]]
implicit val uow = mock[UnitOfWorkContract[Id, List[Todo], Id]]
// . implicit
val service= new TodosService[Id, Id]()
// Id Todo
val list: Id[List[Todo]] = List(Todo(1, "2", 3, Instant.now()))
// getAll uow 1
(tr.getAll _).expects().returning(uow).once()
// commit 1
(uow.commit _).expects().returning(list).once()
// getAll
//
service.getAll() should be(list)
}
}
Scalaでテストを書くのはとても楽しいことがわかり、ScalaCheck、ScalaTest、ScalaMockはとても良いライブラリであることがわかりました。また、API tapirを作成するためのライブラリ、http4sサーバー用のライブラリ、およびfs2ストリーム用のライブラリもあります。これまでのところ、Scalaの環境とライブラリは、私にポジティブな感情をもたらすだけです。この傾向が続くことを願っています。