Apprendre Scala: Partie 3 - Tests unitaires





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





Liens



Sources

Images image du docker



Et donc pour les tests unitaires, vous avez besoin de 3 bibliothèques.



  1. Bibliothèque de création de tests
  2. Bibliothèque qui générera des données de test
  3. 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.



All Articles