Hallo Habr! Es reicht nicht aus, guten Code zu schreiben. Wir müssen es noch mit guten Unit-Tests abdecken. Im letzten Artikel habe ich einen einfachen Webserver erstellt. Jetzt werde ich versuchen zu schreiben, wie viele Tests. Regelmäßig, immobilienbasiert und verspottet. Für Details willkommen unter Katze.
Inhalt
- Scala lernen: Teil 1 - Schlangenspiel
- Lernskala: Teil 2 - Todo Sheet mit Bild-Uploads
- Lernskala: Teil 3 - Unit Tests
Links
Quellen
Bilder Docker-Bild
Für Unit-Tests benötigen Sie 3 Bibliotheken.
- Bibliothek zum Erstellen von Tests
- Bibliothek, die Testdaten generiert
- Eine Bibliothek, die Objekte verspottet
Ich habe die ScalaTest- Bibliothek verwendet, um Tests zu erstellen
"org.scalatest" %% "scalatest" % "3.2.0" % Test
Ich habe ScalaCheck verwendet , um Testdaten für eigenschaftsbasierte Tests zu generieren .
"org.scalacheck" %% "scalacheck" % "1.14.3" % Test
und eine Erweiterung, die ScalaTest + ScalaCheck ScalaTestPlusScalaCheck kombiniert
"org.scalatestplus" %% "scalacheck-1-14" % "3.2.0.0" % Test
Ich habe ScalaMock verwendet, um Objekte zu verspotten
"org.scalamock" %% "scalamock" % "4.4.0" % Test
Eine einfache Klasse, die den Typ der gefüllten (nicht leeren) Zeichenfolge darstellt. Wir werden es jetzt testen.
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
}
}
}
Erstellen einer Klasse für unsere Tests
class FilledStrTests extends AnyFlatSpec with should.Matchers with ScalaCheckPropertyChecks {
}
Wir erstellen eine Methode, die überprüft, ob beim Erstellen unserer Klasse aus denselben Zeilen dieselben Daten empfangen werden.
"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)
}
Im letzten Test haben wir uns in einer manuell erstellten Zeichenfolge versteckt. Lassen Sie uns nun einen Test mit den generierten Daten durchführen. Wir werden einen eigenschaftsbasierten Ansatz verwenden, bei dem die Eigenschaften der Funktion getestet werden, sodass wir mit solchen Eingabedaten solche Ausgabedaten erhalten.
"constructor" should "save expected value" in {
forAll { s: String =>
// . .
whenever(s.trim.nonEmpty) {
val a = FilledStr(s).get
a.value should be(s)
}
}
}
Sie können den Testdatengenerator explizit so konfigurieren, dass nur die von uns benötigten Daten verwendet werden. Zum Beispiel so:
//
val evenInts = for (n <- Gen.choose(-1000, 1000)) yield 2 * n
//
forAll (evenInts) { (n) => n % 2 should equal (0) }
Sie können unseren Generator auch nicht explizit übergeben, sondern dessen Implikation über Arbitrary definieren, sodass er automatisch als Generator an Tests übergeben wird. Zum Beispiel so:
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) }
Sie können auch komplexe Objekte mit Arbitrary generieren.
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 }
Versuchen wir nun, einen Test ernsthafter zu schreiben. Wir werden Abhängigkeiten für TodosService verspotten. Es werden 2 Repositorys verwendet, und das Repository verwendet wiederum eine Abstraktion über die UnitOfWork-Transaktion. Testen wir die einfachste Methode.
def getAll(): F[List[Todo]] =
repo.getAll().commit()
Was nur das Repository aufruft, startet eine Transaktion darin, um die Todo-Liste zu lesen, beendet sie und gibt das Ergebnis zurück. Auch im Test wird anstelle von F [_] die Id-Monade gesetzt, die einfach den darin gespeicherten Wert zurückgibt.
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)
}
}
Es stellte sich als sehr angenehm heraus, Tests in Scala zu schreiben, und ScalaCheck, ScalaTest und ScalaMock erwiesen sich als sehr gute Bibliotheken. Sowie die Bibliothek zum Erstellen von API-Tapir und die Bibliothek für den http4s-Server und die Bibliothek für fs2-Streams. Bisher verursachen die Umgebung und die Bibliotheken für Scala nur positive Emotionen in mir. Ich hoffe, dass sich dieser Trend fortsetzt.