DEV Community

David Goyes
David Goyes

Posted on

Swift Testing #5: Parametrizando pruebas con arguments

Parametrizando una sola condición

En ocasiones, se necesita verificar el funcionamiento de cierto código que recibe un argumento, usando varios valores. La forma tradicional de hacerlo sería construyendo una colección y luego iterando sobre ellas.

@Test("Even Value")
func even() {
  let values = [2, 8, 50]
  values.forEach { value in
    #expect(value.isMultiple(of: 2))
  }
}
Enter fullscreen mode Exit fullscreen mode

Aunque la implementación anterior funciona, tiene algunas desventajas:

  1. En el reporte solo se muestra el resultado global de la ejecución de la prueba. Es decir: si se ejecutan 1000 escenarios y solo de uno de ellos falla, simplemente se muestra la prueba como fallida.
  2. No se puede repetir la prueba con una condición específica, sino que se tienen que ejecutar todas las iteraciones.

Ante esta problemática, las pruebas @Test de Swift Testing pueden ser parametrizadas con una colección. De esta forma, se repite la prueba con cada elemento de la colección, uno a la vez. Con esta aproximación, el reporte de pruebas muestra todos los escenarios por separado y se puede repetir una prueba con unas condiciones específicas, sin tener que ejecutarlas todas.

Para parametrizar una prueba solo hay que poner un parámetro al método de la prueba. Luego, agregar la colección de valores que definen las condiciones de las pruebas como el argumento arguments de @Test. (ver documentación). Gracias a estos cambios, ahora en el reporte de pruebas aparece cada condición discriminada, como si fuera una prueba independiente. Por otro lado, ahora se puede ejecutar la prueba de cada condición por separado.

@Test("Even Value", arguments: [2, 8, 50])
func even(value: Int) {
  #expect(value.isMultiple(of: 2))
}
Enter fullscreen mode Exit fullscreen mode

Probar combinaciones de argumentos

Como extensión del escenario anterior, queremos probar un sistema, combinando las condiciones de dos colecciones. Manualmente lo haríamos anidando dos ciclos como se muestra a continuación.

@Test("Even Value")
func even() {
  let values1 = [2, 8, 50]
  let values2 = [3, 6, 9]
  values1.forEach { value1 in
    values2.forEach { value2 in
      let multiplication = values1 * values2
      #expect(multiplication.isMultiple(of: 2))
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Sin embargo, podemos conseguir el mismo resultado aprovechando la sobrecarga del "trait" arguments que recibe dos colecciones de datos (ver documentación).

@Test("Even Value", arguments: [2, 8, 50], [3, 6, 9])
func even(first: Int, second: Int) {
  let multiplication = first * second
  #expect(multiplication.isMultiple(of: 2))
}
Enter fullscreen mode Exit fullscreen mode

En el sistema anterior de prueba + condiciones, tendríamos un total de nueve escenarios. Sin embargo, ¿qué pasa si no queremos probar todas las combinaciones de las colecciones sino solo uno-a-uno, dado el mismo índice?

Condiciones emparejadas por tupla

Para no combinar colecciones debemos tener una única colección de entrada con N condiciones (con N > 1). Cada condición viene dada por una tupla de M valores, que viene destructurada en M parámetros en el método de la prueba.

En el siguiente ejemplo se va a ejecutar la prueba con dos tuplas de tres elementos. Observar que la prueba tiene tres parámetros, y se ejecuta dos veces porque tenemos dos condiciones.

@Test("Even Value", arguments: [(2, 3, true), (3, 5, false)]
func even(first: Int, second: Int, expectedResult: Bool) {
  let multiplication = first * second
  #expect(multiplication.isMultiple(of: 2) == expectedResult)
}
Enter fullscreen mode Exit fullscreen mode

Debido a la naturaleza de combinación uno-a-uno, se puede usar el operador zip para crear una colección de tipo Zip2Sequence a partir de dos colecciones. Esto permite identificar por separado a las colecciones y por ende, hacer más legible a la prueba. Por ejemplo:

@Test("Even Value", arguments: [zip([2, 3], [3, 4])]
func even(first: Int, second: Int, expectedResult: Bool) {
  let multiplication = first * second
  #expect(multiplication.isMultiple(of: 2))
}
Enter fullscreen mode Exit fullscreen mode

El problema de la implementación anterior es que @Test únicamente permite combinar dos colecciones (ver documentación). Por esta razón, en caso de que sea necesario combinar más de dos colecciones, lo mejor es crear solo una colección compuesta de tuplas de M elementos (con M > 2).

Condiciones emparejadas por estructura

Usar una tupla para emparejar valores de una misma condición es la solución trivial al problema de combinar más de dos colecciones, sin embargo, también se puede conseguir un resultado semejante usando una estructura cuyos atributos sean los valores que componen a la condición de una prueba. Por ejemplo:

struct TestModel {
  let first: Int
  let second: Int
  let result: Int
}
@Test(arguments: [
  TestModel(first: 1, second: 2, result: 3),
  TestModel(first: 2, second: 3, result: 5),
  TestModel(first: 3, second: 4, result: 7),
])
func basic(testModel: TestModel) {
  let result = testModel.first + testModel.second
  #expect(result == testModel.result)
}
Enter fullscreen mode Exit fullscreen mode

Este tipo de aproximación, además de ser más legible en términos de construcción de la prueba, permite agregar una capa más de legibilidad si hacemos que la estructura conforme el protocolo CustomTestStringConvertible para agregar una descripción testDescription.

Por ejemplo, considerar la siguiente estructura TestModel junto con la prueba basic(testModel: TestModel).

struct TestModel: CustomTestStringConvertible {
  let first: Int
  let second: Int
  let result: Int
  var testDescription: String {
    "\(first) + \(second) debería ser igual a \(result)"
  }
}
@Test(arguments: [
  TestModel(first: 1, second: 2, result: 3),
  TestModel(first: 2, second: 3, result: 5),
  TestModel(first: 3, second: 4, result: 7),
])
func basic(testModel: TestModel) {
  let result = testModel.first + testModel.second
  #expect(result == testModel.result)
}
Enter fullscreen mode Exit fullscreen mode

En este caso, el reporte mostraría lo siguiente:

Definitivamente es mucho más legible que el escenario esté descrito como "1 + 2 debería ser igual a 3", que simplemente "70, 100, 170".

Bibliografía

  • Video "Mastering Swift Testing: Eliminate Duplicate Tests with Parameterized Testing" (Swift and Tips), aquí.
  • Documentación sobre Swift Testing, aquí.
  • Documentación "Implementing parameterized tests", aquí.
  • Artículo "Swift Parameterized Testing", aquí.
  • Artículo "Introducing Swift Testing. Parameterized Tests.", aquí.
  • Artículo "Cómo escribir pruebas parametrizadas con Swift Testing", aquí.

Top comments (0)