Bonjour, Habr! Il ne suffit pas d'écrire du bon code. Nous devons encore le couvrir avec de bons tests unitaires. Dans le dernier article, j'ai fait un simple serveur web. Maintenant, je vais essayer d'écrire combien de tests. Régulier, basé sur la propriété et moqué. Pour plus de détails, bienvenue sous chat.
Contenu
- Apprendre Scala: Partie 1 - Jeu de serpent
- Learning Scala: Partie 2 - Feuille Todo avec téléchargement d'images
- Apprendre Scala: Partie 3 - Tests unitaires
Liens
Sources
Images image du docker
Et donc pour les tests unitaires, vous avez besoin de 3 bibliothèques.
- Bibliothèque de création de tests
- Bibliothèque qui générera des données de test
- Une bibliothèque qui se moquera des objets
J'ai utilisé la bibliothèque ScalaTest pour créer des tests
"org.scalatest" %% "scalatest" % "3.2.0" % Test
J'ai utilisé ScalaCheck pour générer des données de test pour les tests basés sur les propriétés .
"org.scalacheck" %% "scalacheck" % "1.14.3" % Test
et une extension qui combine ScalaTest + ScalaCheck ScalaTestPlusScalaCheck
"org.scalatestplus" %% "scalacheck-1-14" % "3.2.0.0" % Test
J'ai utilisé ScalaMock pour simuler des objets
"org.scalamock" %% "scalamock" % "4.4.0" % Test
Une classe simple qui représente le type de chaîne remplie (et non vide). Nous allons maintenant le tester.
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
}
}
}
Créer une classe pour nos tests
class FilledStrTests extends AnyFlatSpec with should.Matchers with ScalaCheckPropertyChecks {
}
Nous créons une méthode qui vérifiera que lors de la création de notre classe à partir des mêmes lignes, nous recevrons les mêmes données.
"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)
}
Dans le dernier test, nous avons codé dans une chaîne fabriquée à la main. Faisons maintenant un test en utilisant les données générées. Nous utiliserons une approche basée sur les propriétés dans laquelle les propriétés de la fonction sont testées, de sorte qu'avec de telles données d'entrée, nous recevrons ces données de sortie.
"constructor" should "save expected value" in {
forAll { s: String =>
// . .
whenever(s.trim.nonEmpty) {
val a = FilledStr(s).get
a.value should be(s)
}
}
}
Vous pouvez configurer explicitement le générateur de données de test pour n'utiliser que les données dont nous avons besoin. Par exemple comme ceci:
//
val evenInts = for (n <- Gen.choose(-1000, 1000)) yield 2 * n
//
forAll (evenInts) { (n) => n % 2 should equal (0) }
Vous pouvez également ne pas passer explicitement notre générateur, mais définir son implicite via Arbitrary afin qu'il soit automatiquement passé en tant que générateur aux tests. Par exemple comme ceci:
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) }
Vous pouvez également générer des objets complexes en utilisant 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 }
Essayons maintenant d'écrire un test plus sérieusement. Nous nous moquerons des dépendances pour TodosService. Il utilise 2 référentiels et le référentiel à son tour utilise une abstraction sur la transaction UnitOfWork. Testons sa méthode la plus simple.
def getAll(): F[List[Todo]] =
repo.getAll().commit()
Ce qui appelle simplement le référentiel, démarre une transaction dans celui-ci pour lire la liste Todo, la termine et renvoie le résultat. Toujours dans le test, au lieu de F [_], la monade Id est placée, qui renvoie simplement la valeur qui y est stockée.
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)
}
}
Ecrire des tests dans Scala s'est avéré très agréable, et ScalaCheck, ScalaTest, ScalaMock se sont avérés être de très bonnes bibliothèques. Ainsi que la bibliothèque de création de tapir d'API et la bibliothèque pour le serveur http4s et la bibliothèque pour les flux fs2. Jusqu'à présent, l'environnement et les bibliothèques de Scala ne provoquent en moi que des émotions positives. J'espère que cette tendance se poursuivra.