Learning Scala: Parte 3 - Pruebas unitarias





¡Hola, Habr! No es suficiente escribir un buen código. Todavía tenemos que cubrirlo con buenas pruebas unitarias. En el último artículo, hice un servidor web simple. Ahora intentaré escribir cuántas pruebas. Regular, basado en la propiedad y burlado. Para más detalles, bienvenido bajo cat.



Contenido





Enlaces



Fuentes

Imágenes Imagen acoplable



Y, por lo tanto, para las pruebas unitarias se necesitan 3 bibliotecas.



  1. Biblioteca para crear pruebas
  2. Biblioteca que generará datos de prueba
  3. Una biblioteca que simulará objetos


Usé la biblioteca ScalaTest para crear pruebas



"org.scalatest" %% "scalatest" % "3.2.0" % Test


Utilicé ScalaCheck para generar datos de prueba para pruebas basadas en propiedades .



"org.scalacheck" %% "scalacheck" % "1.14.3" % Test


y una extensión que combina ScalaTest + ScalaCheck ScalaTestPlusScalaCheck



"org.scalatestplus" %% "scalacheck-1-14" % "3.2.0.0" % Test


Usé ScalaMock para simular objetos



"org.scalamock" %% "scalamock" % "4.4.0" % Test


Una clase simple que representa el tipo de cadena llena (no vacía). Ahora lo probaremos.



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
    }
  }
}


Creando una clase para nuestras pruebas



class FilledStrTests extends AnyFlatSpec with should.Matchers with ScalaCheckPropertyChecks {

}


Creamos un método que comprobará que al crear nuestra clase a partir de las mismas líneas recibiremos los mismos datos.



 "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)
  }


En la última prueba, hemos codificado en una cuerda hecha a mano. Ahora hagamos una prueba con los datos generados. Usaremos un enfoque basado en propiedades en el que se prueban las propiedades de la función, de modo que con dichos datos de entrada, recibamos dichos datos de salida.



  "constructor" should "save expected value" in {
    forAll { s: String =>
//   .          .
      whenever(s.trim.nonEmpty) {
        val a = FilledStr(s).get
        a.value should be(s)
      }
    }
  }


Puede configurar explícitamente el generador de datos de prueba para usar solo los datos que necesitamos. Por ejemplo así:



//   
val evenInts = for (n <- Gen.choose(-1000, 1000)) yield 2 * n
//    
forAll (evenInts) { (n) => n % 2 should equal (0) }


Tampoco puede pasar explícitamente nuestro generador, pero definir su implicación mediante Arbitrario para que se pase automáticamente como generador a las pruebas. Por ejemplo así:



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) }


También puede generar objetos complejos usando 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 }


Ahora intentemos escribir una prueba más en serio. Simularemos dependencias para TodosService. Utiliza 2 repositorios y el repositorio a su vez utiliza una abstracción sobre la transacción UnitOfWork. Probemos su método más simple.



  def getAll(): F[List[Todo]] =
    repo.getAll().commit()


Que simplemente llama al repositorio, inicia una transacción en él para leer la lista de Todo, la finaliza y devuelve el resultado. También en la prueba, en lugar de F [_], se pone la mónada Id, que simplemente devuelve el valor almacenado en ella.



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)
  }
}


Resultó muy agradable escribir pruebas en Scala, y ScalaCheck, ScalaTest, ScalaMock resultaron ser bibliotecas muy buenas. Así como la biblioteca para crear API tapir y la biblioteca para el servidor http4s y la biblioteca para flujos fs2. Hasta ahora, el entorno y las bibliotecas de Scala solo me causan emociones positivas. Espero que esta tendencia continúe.



All Articles