MUnit I: declaración de test

Inicio una serie de entradas relacionadas MUnit. MUnit es una librería de testing para Scala. En la presente entrada, MUnit 1: declaración de test, realizaré una introducción y una descripción de cómo se declaran test con esta librería.

MUnit tiene una parecido a otra librería de testing como es JUnit. JUnit es una librería del contexto del lenguaje de programación Java. La filosofía de MUnit es seguir la misma línea de JUnit pero en el contenxto del lengua Scala. Las características de MUnit son las siguientes:

  1. MUnit se implementa como un ejecutor de JUnit e intenta construir sobre JUnit siempre que sea posible.
  2. No tiene dependencias externas con otras librerías del mundo Scala.
  3. Es una librería multiplataforma que complica MUnit a JVM a través de JavaScript Scala.js.
  4. Informes de pruebas entendibles para analizar los problemas.

Definición de dependencias

La definición de dependencias de MUnit en un proyecto Scala en donde se utiliza sbt se realiza de la siguiente manera:

val MunitVersion = "0.7.20"
val MunitCatsEffectVersion = "0.12.0"
[...]
lazy val root = (project in file("."))
  .settings(
     scalacOptions += "-Yrangepos",
     libraryDependencies ++= Seq(
        "org.scalameta" %% "munit" % MunitVersion % Test,
        "org.typelevel" %% "munit-cats-effect-2" % MunitCatsEffectVersion % Test
     ),
  testFrameworks += new TestFramework("munit.Framework")
)

Ejemplo básico de test con MUnit

La definición de un test en MUnit y la forma de trabajar es parecida a JUnit. Los test se definen heredando de la clase munit.FunSuite, se declara un test con la sentencia test y, dentro de esta, se define la prueba funcional verificando el resultado con assertEquals. Un ejemplo básico de test es el descrito en el siguiente snippet:

class BasicEjem1 extends munit.FunSuite {
  test("Basic") {
    val obtained = 43
    val expected = 43
    assertEquals(obtained, expected)
  }
}

Declaración de test en MUnit

Declaración de test asíncronos.

MUnit tiene la capacidad de realizar test de tareas asíncronas. Esta tarea es posible gracias a los transformadores de MUnit los cuáles permiten controlar la ejecución de este tipo de tarea con un tiempo límite de espera de 30 segundos.

En el siguiente ejemplo, se muestra la definición de un test asíncrono cuyo tiempo de espera es el valor por defecto.

import scala.concurrent.Future
class BasicAsyncEjem1 extends munit.FunSuite {
  implicit val ec = scala.concurrent.ExecutionContext.global
  test("async") {
    Future {
      println("Hello World")
    }
  }
}

Si deseamos modificar el timeout, debemos de modificar el valor del atributo munitTimeout. Un ejemplo de modificación del campo munitTimeout es el siguiente:

import scala.concurrent.Future
import scala.concurrent.duration.Duration
class BasicAsyncEjem2 extends munit.FunSuite {
  implicit val ec = scala.concurrent.ExecutionContext.global
  override val munitTimeout = Duration(1, "s")
  test("slow-async") {
    Future {
      Thread.sleep(5000)
      println("Hello world, slow-async")
    }
  }
}

La salida por consola es la excepción de Timeout cuya traza es la siguiente:

java.util.concurrent.TimeoutException: Future timed out after [1 second]

MUnit tiene una tratamiento especial para manejar Future, pudiendo definir tareas asíncronas sin necesidad de ser ejecutas con un resultado de éxito del test. En el siguiente ejemplo, se muestra una tarea asíncrona definida en un case class que nunca se ejecuta y el resultado de ejecución es correcto.

class BasicAsyncEjem3 extends munit.FunSuite {
  implicit val ec = scala.concurrent.ExecutionContext.global
  case class LazyFuture[+T](run: () => Future[T])
  object LazyFuture {
    def apply[T](thunk: => T)(implicit ec: ExecutionContext): LazyFuture[T] =
      LazyFuture(() => Future(thunk))
  }
  test("buggy-task") {
    LazyFuture {
      Thread.sleep(10)
      println("Hello world BasicAsyncEjem3")
    }
  }
}

Para poder ejecutar la anterior tarea asíncrona, necesitamos definir un ejecutor que sea capaz de ejecutar la referencia a la función run. En el siguiente ejemplo, definimos dicho ejecutor identificado como munitValueTransform. El código del ejemplo es el siguiente:

import scala.concurrent.{ExecutionContext, Future}
class BasicAsyncEjem4 extends munit.FunSuite {
  case class LazyFuture[+T](run: () => Future[T])
  object LazyFuture {
    def apply[T](thunk: => T)(implicit ec: ExecutionContext): LazyFuture[T] =
      LazyFuture(() => Future(thunk))
  }
  override def munitValueTransforms = super.munitValueTransforms ++ List(
    new ValueTransform(
      "LazyFuture",
      { 
        case LazyFuture(run) => run()
      }
    )
  )
  implicit val ec = ExecutionContext.global
    test("tarea OK") {
      LazyFuture {
         Thread.sleep(5000)
         println("Hello world BasicAsyncEjem4")
      }
    }
}

La salida por consola del código anterior es la siguiente:

Hello world BasicAsyncEjem4

Definir test con una función auxiliar.

Podemos definir una función que defina un test cuyos parámetros pueden ser por valor o por nombre. Un código ejemplo de estos test es el siguiente:

class AuxiliarFunctionEjem1 extends munit.FunSuite {
   def check[T](name: String, original: List[T], expected: Option[T])(implicit loc: munit.Location): Unit = {
      test(name) {
         val obtained = original.headOption
         assertEquals(obtained, expected)
      }
   }
   check("basic", List(1, 2), Some(1))
   def checkByName(name: String, bytes: => Array[Byte]): Unit =
       test(name) {
          assertEquals(bytes.length > 0, true)
       }
   import java.nio.file.{Files, Paths}
   checkByName("file", Files.readAllBytes(Paths.get("build.sbt")))
}

Definir test que siempre falle.

En ocasiones necesitamos que un test falle. Para definir este comportamiento, empleamos la función fail. Un código ejemplo es el siguiente:

class AlwaysFailEjem1 extends munit.FunSuite {
  test("issue-456".fail) {
     assertEquals(1, 1)
  }
}

Modificar el comportamiento de ejecutor munitTest

Si necesitamos modificar el comportamiento de un test empleamos un tag. Un tag es una entidad de Munit la cual se representa con un case class. El comportamiento del test lo definiremos en una clase TestTransform del ejecutor munitTestTransforms, referenciando al tag identificador del comportamiento. En la identificación del test, se define la referencia al tag a ejecutar. Un código ejemplo para ejecutar un test cuatro veces es el siguiente:

case class Rerun(count: Int) extends munit.Tag("Rerun")
class CustomizeEvaluationTestEjem1 extends munit.FunSuite {
   override def munitTestTransforms = super.munitTestTransforms ++ List(
     new TestTransform(
       "Rerun",
       { test =>
           val rerunCount = test.tags.collectFirst { case Rerun(n) => n }.getOrElse(1)
           if (rerunCount == 1) test
           else {
             test.withBody(() => {
                Future.sequence(1.to(rerunCount).map(_ => test.body()).toList)
             })
           }
       }
     )
   )
   test("files".tag(Rerun(4))) {
       println(s"Hello world...")
   }
}

El resultado del anterior código es la escritura en consola del texto «Hello world…» cuatro veces.

Modificar el nombre de un test de forma dinámica.

Si necesitamos ejecutar test cuyo nombre dependa de circunstancias externas, es necesario emplear un TestTransform. Un código ejemplo que modifica el nombre de los test de forma dinámica con la versión de scala es el siguiente:

class CustomizeTestNameEjem1 extends munit.FunSuite {
  val scalaVersion = scala.util.Properties.versionNumberString
  override def munitTestTransforms: List[TestTransform] = super.munitTestTransforms ++ List(
     new TestTransform(
        "append Scala version",
        { test =>
            test.withName(test.name + "-" + scalaVersion)
        }
      )
  )
  test("test-Foo") {
     assert(scalaVersion.startsWith("2.13.4"))
     assertEquals(scalaVersion, "2.13.4")
  }
}

El resultado de la ejecución es el que se muestra en la siguiente imagen:

Saltar un test.

Si necesitamos saltar un test, emplearemos la función flaky para saltar la ejecución. Un código ejemplo es el siguiente:

class FlakyTestEjem1 extends munit.FunSuite {
  test("requests".flaky) {
    val obtained = 42
    val expected = 43
    assertEquals(obtained, expected)
  }
}

Para saltar la ejecución en los test, es necesario ejecutar sbt definiendo la variable MUNIT_FLASK_OK con valor a true. Si no se emplea la variable, los test se ejecutan normalmente. Un ejemplo de ejecución es el siguiente:

MUNIT_FLAKY_OK=true sbt test

Compartir configuración de test.

Si necesitamos definir clases de test con componentes comunes, se define una herencia de clases cuya clase base defina todo aquello que sea común. Si necesitamos modificar el nombre de los test de dos clases diferentes, definimos una clase base con el transformador del nombre de test y, sus clases hijas, heredan de dicha clase base. Un código ejemplo es el siguiente:

class BaseSuite extends munit.FunSuite {
   val scalaVersion = scala.util.Properties.versionNumberString
   override def munitTestTransforms: List[TestTransform] = super.munitTestTransforms ++ List(
      new TestTransform(
        "append Scala version",
        { test =>
          test.withName(test.name + "-" + scalaVersion)
        }
      )
   )
}
class MyFirstTestSuite extends BaseSuite {
   test("FirstTestSuite.test1") {
      println("Hello world ...FirstTestSuite.test1")
   }
   test("FirstTestSuite.test2") {
      println("Hello world FirstTestSuite.test2")
   }
}
class MySecondTestSuite extends BaseSuite {
   test("SecondTestSuite.test1") {
      println("Hello world ...SecondTestSuite.test1")
   }
   test("SecondTestSuite.test2") {
      println("Hello world SecondTestSuite.test2")
   }
}

La salida por consola es la que se muestra en la siguiente imagen:

Definición de una batería de pruebas.

Si necesitamos definir una batería de pruebas, definimos una clase de test que herede de munit.Suite, definimos el tipo de de valor de las pruebas y, para finalizar, definimos la lista de test con la creación de tantas clases Test como queremos. Un código de ejemplo es el siguiente:

import munit.Assertions.assertEquals
import munit.{Location, Tag}
class TestLibrarySuite2 extends munit.Suite {
   override type TestValue = Unit
   override def munitTests(): Seq[Test] = List(
     new Test(
       "example1",
       body = () => {
          val obtained = 43
          val expected = 43
          assertEquals(obtained, expected)
       },
       tags = Set.empty[Tag],
       location = implicitly[Location]
     ),
     new Test(
        "example2",
        body = () => {
           val obtained = "43"
           val expected = "43"
           assertEquals(obtained, expected)
        },
        tags = Set.empty[Tag],
        location = implicitly[Location]
     )
   )
}

En la siguiente entrada, MUnit II: aserciones, describiré en cómo definir aserciones en los test.

Deja un comentario