Cómo probar el código de lanzamiento en Swift

Todo lo que necesita saber acerca de probar las funciones de lanzamiento con XCTest y mantener el código de prueba limpio y robusto en el proceso.

¿Cuántas veces ha tenido que hacerse cargo de un proyecto donde había pruebas unitarias, pero eran difíciles de comprender, fallaban desesperadamente o el objetivo de la prueba ni siquiera se construía?

Es crucial mantener el código de prueba de unidad robusto y fácil de mantener, no permitir que sean abandonados e ignorados con el tiempo.

En Storytel tratamos de hacer que nuestras pruebas unitarias sean cortas y legibles. Debido a su naturaleza, el código de prueba tiende a ser largo y repetitivo, por lo que es importante mantenerlo limpio, luego trabajar con las pruebas no se vuelve demasiado tedioso a medida que el proyecto crece.

El código que arroja puede ser difícil de probar a veces, y el código de prueba resultante puede terminar feo. En este artículo me sumergiré en diferentes escenarios y cómo resolverlos de una manera agradable y robusta.

XCTest es un marco poderoso. En Xcode 8.3, Apple introdujo algunas funciones nuevas de XCTAssert además de un par de docenas de funciones existentes. Si bien las características que proporcionan permiten hacer la mayoría de las cosas que un desarrollador desearía, algunas cosas aún requieren un código repetitivo además de las características proporcionadas de forma nativa.

Echemos un vistazo a algunos casos y cómo los resolvemos.

Comparar resultados de funciones de lanzamiento

Este es fácil. Todas las funciones XCTAssert existentes ya toman argumentos de lanzamiento. Si el resultado es Equatable, una línea como esta hará el trabajo:

XCTAssertEqual (prueba x.calculateValue (), expectValue)

Si CalculateValue () arroja un error, la prueba falla con un mensaje "XCTAssertEqual falló: arrojó un error ...". Si la llamada no se lanzó y dos valores no son iguales, la prueba falló con el mensaje “XCTAssertEqual falló: a no es igual a b”, donde 'a' y 'b' son descripciones de argumentos izquierdo y derecho respectivamente, producidos por Cadena (que describe :).
Simplemente hablando, todas las funciones de verificación de valor de XCTAssert * verifican que una llamada no arroje ningún error y devuelva el resultado esperado. Muy conveniente.

Funciones con tipos de retorno no equiparables

A menudo, XCTAssertEqual y sus hermanos de verificación de valor no son suficientes.

Si una función no devuelve un valor o si se puede ignorar en el contexto de un caso de prueba, podemos usar la nueva función XCTAssertNoThrow que estuvo disponible en Xcode 8.3:

XCTAssertNoThrow (prueba x.doSomething ())

Curiosamente, antes de que saliera Xcode 8.3 teníamos una función personalizada que tenía exactamente la misma firma, y ​​no teníamos que cambiar ningún código excepto eliminar nuestra implementación personalizada.

Otro caso común es cuando el tipo devuelto no es Equatable, o si queremos verificar solo algunas propiedades del resultado devuelto.
Incluso si el tipo es Equatable, la falla del caso de prueba de igualdad es casi inútil cuando la descripción del objeto es muy larga: es difícil saber qué campo tiene un valor incorrecto:

En nuestros proyectos tenemos muchas funciones que producen tipos complejos que no se ajustan a Equatable. Un ejemplo común son los objetos del modelo de datos: los exponemos como protocolos para ocultar la implementación interna y no queremos que sean equiparables. Cada tipo de modelo tiene un inicializador de lanzamiento que toma un diccionario.

En algún momento nos dimos cuenta de que nuestras pruebas unitarias parecen horribles. Tenían código repetitivo, opciones sin sentido y cuando esas pruebas fallaban, chico, todo era rojo. Para empeorar las cosas, había un montón de copiar y pegar. El código se veía así:

XCTAssertNoThrow (pruebe BookObject (diccionario: sampleDictionary))
dejar libro = probar? BookObject (diccionario: sampleDictionary)
XCTAssertEqual (libro? .Name, "...")
XCTAssertEqual (libro? .Description, "...")
XCTAssertEqual (libro? .Rating, 5)
...
// versión "mejor":
XCTAssertNoThrow (pruebe BookObject (diccionario: sampleDictionary))
XCTAssertEqual (intente BookObject (diccionario: sampleDictionary) .name, "...")
XCTAssertEqual (pruebe BookObject (diccionario: sampleDictionary) .description, "...")
XCTAssertEqual (pruebe BookObject (diccionario: sampleDictionary) .rating, 5)
...
No son nueve fallas, es una ...

La solución resultó ser simple. El truco consistía en extraer un resultado producido por el primer cierre automático de XCTAssertNoThrow y luego ejecutar un cierre de validación adicional, pero solo en caso de que hubiera un resultado.

func pública XCTAssertNoThrow  (_ expresión: @autoclosure () arroja -> T, _ mensaje: String = "", archivo: StaticString = #file, línea: UInt = #line, también validateResult: (T) -> Void ) {
    func executeAndAssignResult (_ expresión: @autoclosure () arroja -> T, a: inout T?) vuelve a lanzar {
        to = probar expresión ()
    }
    resultado var: T?
    XCTAssertNoThrow (intente executeAndAssignResult (expresión, para: y resultado), mensaje, archivo: archivo, línea: línea)
    si let r = result {
        validateResult (r)
    }
}

Ahora las mismas pruebas parecían mucho más razonables: legibles, fuertemente tipadas y produciendo mensajes digeribles.

Lanzamiento de errores específicos.

En algunos casos, queremos probar lo contrario: que una función arroja un error. Esto a menudo es necesario para probar la deserialización del modelo.
Ya podríamos hacer eso con la función existente XCTAssertThrowsError, aunque si quisiéramos comprobar que se produjo algún error específico, tendríamos que proporcionar un cierre para evaluar el error arrojado.

Al observar qué tipo de controles usualmente teníamos allí, notamos que son solo dos: ya sea comparando el error devuelto con el esperado, o simplemente verificando su tipo. Así que creamos dos funciones convenientes para convertir esas pruebas en frases simples:

func pública XCTAssertThrowsError  (_ expresión: @autoclosure () lanza -> T, esperadoError: E, _ mensaje: String = "", archivo: StaticString = #file, line: UInt = #line ) {
    XCTAssertThrowsError (try expression (), message, file: file, line: line, {(error) en
        XCTAssertNotNil (error como? E, "\ (error) no es \ (E.self)", archivo: archivo, línea: línea)
        XCTAssertEqual (error como? E, error esperado, archivo: archivo, línea: línea)
    })
}
func pública XCTAssertThrowsError  (_ expresión: @autoclosure () arroja -> T, esperadoErrorType: E.Type, _ message: String = "", file: StaticString = #file, line: UInt = #line ) {
    XCTAssertThrowsError (try expression (), message, file: file, line: line, {(error) en
        XCTAssertNotNil (error como? E, "\ (error) no es \ (E.self)", archivo: archivo, línea: línea)
    })
}

Incluso si no arroja ...

El poder de las funciones de lanzamiento se puede usar para escribir pruebas más robustas para otros escenarios también, donde la igualdad regular no es aplicable.

Considere tener una enumeración que no pueda hacerse equiparable, por ejemplo, si los valores asociados de sus casos no son equiparables.
En lugar de cambiar en casos de prueba, escribimos funciones "auxiliares" puras que arrojan errores significativos.

Un ejemplo común es una enumeración de resultados:

enum Resultado {
    caso de éxito ([String: Any])
    falla de caso (error)
}

Si estuviéramos probando una función que devuelve Resultado directamente, tendríamos que cambiar el valor devuelto y llamar a XCTFail en casos incorrectos. Copiaríamos y pegaríamos el interruptor para cada caso de prueba, y actualizar las pruebas para nuevos casos de enumeración sería una pesadilla.

En su lugar, podemos crear funciones de lanzamiento de ayuda para manejar la enumeración en un solo lugar:

XCTAssert (pruebe result.assertIsSuccess (afirmValue: {(valor: [String: Any]) en
    XCTAssertEqual (value.count, 10)
}))
XCTAssert (pruebe result.assertIsFailure (afirmarError: {(valor: Error) en
    XCTAssertEquals (valor, MyError.case)
}))
// MARK: ayudantes
Extensión privada Resultado {
    Error de enumeración privada: Swift.Error, CustomStringConvertible {
        mensaje var: cadena
        descripción var: Cadena {mensaje de retorno}
    }
    func ClaimIsSuccess (afirmValue: (([String: Any]) throws -> Void)? = nil) throws -> Bool {
        cambiar a sí mismo {
        case .success (let value):
            intente afirmar valor? (valor)
            volver verdadero
        caso .failure (_):
            error de lanzamiento (mensaje: "éxito esperado, conseguido. \ (self)")
        }
    }
    func afirmarIsFailure (afirmarError: ((Error) throws -> Void)? = nil) throws -> Bool {
        cambiar a sí mismo {
        case .success (_):
            Lanzar error (mensaje: "Error esperado, conseguido. \ (self)")
        caso .failure (dejar error):
            intente afirmar el error? (valor)
            volver verdadero
        }
    }
}

Este tipo de enfoque se puede usar para varios escenarios, como verificar con gracia las opciones (que también son enumeraciones: troll :).

Una nota sobre la creación de funciones de aserción personalizadas

Hay pocas cosas a tener en cuenta al escribir funciones de prueba personalizadas.

  1. Es una buena práctica agregar argumentos de línea y archivo, pasándolos a las funciones estándar de XCTAssert. De esta forma, los fallos de los casos de prueba se informan en el punto donde se llama a su aserción personalizada, y no en el cuerpo de la función en sí.
  2. Es bueno agregar un parámetro de mensaje para que la persona que llama pueda proporcionar contexto a la prueba. También es bueno usarlos al escribir casos de prueba :)
  3. XCTFail (mensaje :) proporciona una forma de reprobar incondicionalmente una prueba, que puede ser muy útil, p. al probar enumeraciones no equiparables y caer en un caso inesperado.

* *

El marco XCTest ha crecido hasta ser muy poderoso en el último año, y tratamos de mantener la reutilización de las funciones existentes tanto como sea posible en lugar de replicar su comportamiento.

Vale la pena señalar que para NSExceptions, el marco XCTest proporciona una API más rica, que desafortunadamente solo está disponible en Objective-C: https://developer.apple.com/documentation/xctest/nsexception_assertions?language=objc

La documentación completa para todas las funciones de aserción se puede encontrar aquí: https://developer.apple.com/documentation/xctest

Originalmente inspirado en esto: http://ericasadun.com/2017/05/27/tests-that-dont-crash/

* *

Actualización para Xcode 9: todavía no se han agregado nuevas API de afirmación. ¡Espero tener soporte para NSExceptions en la versión Swift de XCTest!

* *

Actualización para Xcode 10.2: no se han agregado nuevas API de afirmación.

Comencé a investigar cómo puedo contribuir a la implementación de código abierto y creé un discurso en los foros de Swift. ¿Quieres ayudarme con esto? Comuníquese conmigo aquí o en Twitter, ¡hagamos que XCTest sea mejor juntos!

¡Gracias por leer! Si disfrutaste el artículo, compártelo haciendo clic en el botón Compartir debajo del artículo; más personas se beneficiarán de él.

También puedes seguirme en Twitter, donde escribo sobre todo sobre el desarrollo de iOS.

Quieres trabajar conmigo Storytel está contratando - solicite aquí