unit testing - test - Prueba de afirmación en Swift
unit test swift (6)
Creo que a partir de Beta6 todavía es imposible para Swift detectar una excepción directamente. La única forma en que puede manejar esto es escribir ese caso de prueba particular en ObjC.
Dicho esto, tenga en cuenta que _XCTAssertionType.Throws
sí existe, lo que sugiere que el equipo Swift es consciente de esto y tiene la intención de proporcionar una solución. Es bastante imaginable que puedas escribir esta afirmación tú mismo en ObjC y exponerla a Swift (no puedo pensar en ninguna razón que sea imposible en Beta6). El gran problema es que es posible que no pueda obtener fácilmente una buena información de ubicación (la línea específica que falló, por ejemplo).
Estoy escribiendo pruebas unitarias para un método que tiene una aserción. La guía de idiomas de Swift recomienda el uso de aserciones para "condiciones inválidas":
Las afirmaciones hacen que su aplicación finalice y no sustituyen el diseño de su código de tal manera que es poco probable que surjan condiciones no válidas. No obstante, en situaciones donde las condiciones no válidas son posibles, una afirmación es una forma efectiva de garantizar que dichas condiciones se destacan y se detectan durante el desarrollo, antes de que se publique su aplicación.
Quiero probar el caso de fallo.
Sin embargo, no hay XCTAssertThrows
en Swift (a partir de Beta 6). ¿Cómo puedo escribir una prueba de unidad que pruebe que una afirmación falla?
Editar
Según la sugerencia de @ RobNapier, intenté envolver XCTAssertThrows
en un método Objective-C y llamar a este método desde Swift. Esto no funciona, ya que la macro no detecta el error fatal causado por assert
y, por lo tanto, la prueba se bloquea.
De acuerdo con el comentario de nschum de que no parece correcto hacer una prueba de prueba unitaria porque, de forma predeterminada, no estará en el código prod. Pero si realmente quería hacerlo, aquí está la versión de assert
para referencia:
anular aseverar
func assert(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = "", file: StaticString = __FILE__, line: UInt = __LINE__) {
assertClosure(condition(), message(), file, line)
}
var assertClosure: (Bool, String, StaticString, UInt) -> () = defaultAssertClosure
let defaultAssertClosure = {Swift.assert($0, $1, file: $2, line: $3)}
extensión auxiliar
extension XCTestCase {
func expectAssertFail(expectedMessage: String, testcase: () -> Void) {
// arrange
var wasCalled = false
var assertionCondition: Bool? = nil
var assertionMessage: String? = nil
assertClosure = { condition, message, _, _ in
assertionCondition = condition
assertionMessage = message
wasCalled = true
}
// act
testcase()
// assert
XCTAssertTrue(wasCalled, "assert() was never called")
XCTAssertFalse(assertionCondition!, "Expected false to be passed to the assert")
XCTAssertEqual(assertionMessage, expectedMessage)
// clean up
assertClosure = defaultAssertClosure
}
}
El proyecto CwlPreconditionTesting de Matt Gallagher en github agrega una función catchBadInstruction
que le brinda la posibilidad de probar fallas de afirmación / condición previa en el código de prueba de unidad.
El archivo CwlCatchBadInstructionTests muestra una ilustración simple de su uso. (Tenga en cuenta que solo funciona en el simulador para iOS).
Gracias a y por la idea detrás de esta respuesta.
Aquí hay una idea de cómo hacerlo.
Aquí hay un proyecto de ejemplo.
Esta respuesta no es sólo para afirmar. También es para los otros métodos de aserción ( assertionFailure
, assertionFailure
, precondition
, preconditionFailure
precondition
, fatalError
y fatalError
)
1. Coloque ProgrammerAssertions.swift
en el destino de su aplicación o marco bajo prueba. Justo al lado de su código fuente.
ProgrammerAssertions.swift
import Foundation
/// drop-in replacements
public func assert(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = "", file: StaticString = __FILE__, line: UInt = __LINE__) {
Assertions.assertClosure(condition(), message(), file, line)
}
public func assertionFailure(@autoclosure message: () -> String = "", file: StaticString = __FILE__, line: UInt = __LINE__) {
Assertions.assertionFailureClosure(message(), file, line)
}
public func precondition(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = "", file: StaticString = __FILE__, line: UInt = __LINE__) {
Assertions.preconditionClosure(condition(), message(), file, line)
}
@noreturn public func preconditionFailure(@autoclosure message: () -> String = "", file: StaticString = __FILE__, line: UInt = __LINE__) {
Assertions.preconditionFailureClosure(message(), file, line)
runForever()
}
@noreturn public func fatalError(@autoclosure message: () -> String = "", file: StaticString = __FILE__, line: UInt = __LINE__) {
Assertions.fatalErrorClosure(message(), file, line)
runForever()
}
/// Stores custom assertions closures, by default it points to Swift functions. But test target can override them.
public class Assertions {
public static var assertClosure = swiftAssertClosure
public static var assertionFailureClosure = swiftAssertionFailureClosure
public static var preconditionClosure = swiftPreconditionClosure
public static var preconditionFailureClosure = swiftPreconditionFailureClosure
public static var fatalErrorClosure = swiftFatalErrorClosure
public static let swiftAssertClosure = { Swift.assert($0, $1, file: $2, line: $3) }
public static let swiftAssertionFailureClosure = { Swift.assertionFailure($0, file: $1, line: $2) }
public static let swiftPreconditionClosure = { Swift.precondition($0, $1, file: $2, line: $3) }
public static let swiftPreconditionFailureClosure = { Swift.preconditionFailure($0, file: $1, line: $2) }
public static let swiftFatalErrorClosure = { Swift.fatalError($0, file: $1, line: $2) }
}
/// This is a `noreturn` function that runs forever and doesn''t return.
/// Used by assertions with `@noreturn`.
@noreturn private func runForever() {
repeat {
NSRunLoop.currentRunLoop().run()
} while (true)
}
2. XCTestCase+ProgrammerAssertions.swift
en su objetivo de prueba. Justo aparte de tus casos de prueba.
XCTestCase + ProgrammerAssertions.swift
import Foundation
import XCTest
@testable import Assertions
private let noReturnFailureWaitTime = 0.1
public extension XCTestCase {
/**
Expects an `assert` to be called with a false condition.
If `assert` not called or the assert''s condition is true, the test case will fail.
- parameter expectedMessage: The expected message to be asserted to the one passed to the `assert`. If nil, then ignored.
- parameter file: The file name that called the method.
- parameter line: The line number that called the method.
- parameter testCase: The test case to be executed that expected to fire the assertion method.
*/
public func expectAssert(
expectedMessage: String? = nil,
file: StaticString = __FILE__,
line: UInt = __LINE__,
testCase: () -> Void
) {
expectAssertionReturnFunction("assert", file: file, line: line, function: { (caller) -> () in
Assertions.assertClosure = { condition, message, _, _ in
caller(condition, message)
}
}, expectedMessage: expectedMessage, testCase: testCase) { () -> () in
Assertions.assertClosure = Assertions.swiftAssertClosure
}
}
/**
Expects an `assertionFailure` to be called.
If `assertionFailure` not called, the test case will fail.
- parameter expectedMessage: The expected message to be asserted to the one passed to the `assertionFailure`. If nil, then ignored.
- parameter file: The file name that called the method.
- parameter line: The line number that called the method.
- parameter testCase: The test case to be executed that expected to fire the assertion method.
*/
public func expectAssertionFailure(
expectedMessage: String? = nil,
file: StaticString = __FILE__,
line: UInt = __LINE__,
testCase: () -> Void
) {
expectAssertionReturnFunction("assertionFailure", file: file, line: line, function: { (caller) -> () in
Assertions.assertionFailureClosure = { message, _, _ in
caller(false, message)
}
}, expectedMessage: expectedMessage, testCase: testCase) { () -> () in
Assertions.assertionFailureClosure = Assertions.swiftAssertionFailureClosure
}
}
/**
Expects an `precondition` to be called with a false condition.
If `precondition` not called or the precondition''s condition is true, the test case will fail.
- parameter expectedMessage: The expected message to be asserted to the one passed to the `precondition`. If nil, then ignored.
- parameter file: The file name that called the method.
- parameter line: The line number that called the method.
- parameter testCase: The test case to be executed that expected to fire the assertion method.
*/
public func expectPrecondition(
expectedMessage: String? = nil,
file: StaticString = __FILE__,
line: UInt = __LINE__,
testCase: () -> Void
) {
expectAssertionReturnFunction("precondition", file: file, line: line, function: { (caller) -> () in
Assertions.preconditionClosure = { condition, message, _, _ in
caller(condition, message)
}
}, expectedMessage: expectedMessage, testCase: testCase) { () -> () in
Assertions.preconditionClosure = Assertions.swiftPreconditionClosure
}
}
/**
Expects an `preconditionFailure` to be called.
If `preconditionFailure` not called, the test case will fail.
- parameter expectedMessage: The expected message to be asserted to the one passed to the `preconditionFailure`. If nil, then ignored.
- parameter file: The file name that called the method.
- parameter line: The line number that called the method.
- parameter testCase: The test case to be executed that expected to fire the assertion method.
*/
public func expectPreconditionFailure(
expectedMessage: String? = nil,
file: StaticString = __FILE__,
line: UInt = __LINE__,
testCase: () -> Void
) {
expectAssertionNoReturnFunction("preconditionFailure", file: file, line: line, function: { (caller) -> () in
Assertions.preconditionFailureClosure = { message, _, _ in
caller(message)
}
}, expectedMessage: expectedMessage, testCase: testCase) { () -> () in
Assertions.preconditionFailureClosure = Assertions.swiftPreconditionFailureClosure
}
}
/**
Expects an `fatalError` to be called.
If `fatalError` not called, the test case will fail.
- parameter expectedMessage: The expected message to be asserted to the one passed to the `fatalError`. If nil, then ignored.
- parameter file: The file name that called the method.
- parameter line: The line number that called the method.
- parameter testCase: The test case to be executed that expected to fire the assertion method.
*/
public func expectFatalError(
expectedMessage: String? = nil,
file: StaticString = __FILE__,
line: UInt = __LINE__,
testCase: () -> Void) {
expectAssertionNoReturnFunction("fatalError", file: file, line: line, function: { (caller) -> () in
Assertions.fatalErrorClosure = { message, _, _ in
caller(message)
}
}, expectedMessage: expectedMessage, testCase: testCase) { () -> () in
Assertions.fatalErrorClosure = Assertions.swiftFatalErrorClosure
}
}
// MARK:- Private Methods
private func expectAssertionReturnFunction(
functionName: String,
file: StaticString,
line: UInt,
function: (caller: (Bool, String) -> Void) -> Void,
expectedMessage: String? = nil,
testCase: () -> Void,
cleanUp: () -> ()
) {
let expectation = expectationWithDescription(functionName + "-Expectation")
var assertion: (condition: Bool, message: String)? = nil
function { (condition, message) -> Void in
assertion = (condition, message)
expectation.fulfill()
}
// perform on the same thread since it will return
testCase()
waitForExpectationsWithTimeout(0) { _ in
defer {
// clean up
cleanUp()
}
guard let assertion = assertion else {
XCTFail(functionName + " is expected to be called.", file: file.stringValue, line: line)
return
}
XCTAssertFalse(assertion.condition, functionName + " condition expected to be false", file: file.stringValue, line: line)
if let expectedMessage = expectedMessage {
// assert only if not nil
XCTAssertEqual(assertion.message, expectedMessage, functionName + " called with incorrect message.", file: file.stringValue, line: line)
}
}
}
private func expectAssertionNoReturnFunction(
functionName: String,
file: StaticString,
line: UInt,
function: (caller: (String) -> Void) -> Void,
expectedMessage: String? = nil,
testCase: () -> Void,
cleanUp: () -> ()
) {
let expectation = expectationWithDescription(functionName + "-Expectation")
var assertionMessage: String? = nil
function { (message) -> Void in
assertionMessage = message
expectation.fulfill()
}
// act, perform on separate thead because a call to function runs forever
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), testCase)
waitForExpectationsWithTimeout(noReturnFailureWaitTime) { _ in
defer {
// clean up
cleanUp()
}
guard let assertionMessage = assertionMessage else {
XCTFail(functionName + " is expected to be called.", file: file.stringValue, line: line)
return
}
if let expectedMessage = expectedMessage {
// assert only if not nil
XCTAssertEqual(assertionMessage, expectedMessage, functionName + " called with incorrect message.", file: file.stringValue, line: line)
}
}
}
}
3. Use assert
, assertionFailure
, precondition
, preconditionFailure
y fatalError
normalmente como siempre hace.
Por ejemplo: si tiene una función que hace una división como la siguiente:
func divideFatalError(x: Float, by y: Float) -> Float {
guard y != 0 else {
fatalError("Zero division")
}
return x / y
}
4. expectAssert
prueba de unidad con los nuevos métodos expectAssert
, expectAssertionFailure
, expectPrecondition
, expectPreconditionFailure
y expectFatalError
.
Puedes probar la división 0 con el siguiente código.
func testFatalCorrectMessage() {
expectFatalError("Zero division") {
divideFatalError(1, by: 0)
}
}
O si no quieres probar el mensaje, simplemente lo haces.
func testFatalErrorNoMessage() {
expectFatalError() {
divideFatalError(1, by: 0)
}
}
Tenemos código Swift (4) que prueba un marco Objective-C. Algunos de los métodos del marco llaman a NSAssert
.
Inspirado por NSHipster , terminé con una implementación como la siguiente:
SwiftAssertionHandler.h (use esto en un encabezado de puente)
@interface SwiftAssertionHandler : NSAssertionHandler
@property (nonatomic, copy, nullable) void (^handler)(void);
@end
SwiftAssertionHandler.m
@implementation SwiftAssertionHandler
- (instancetype)init {
if (self = [super init]) {
[[[NSThread currentThread] threadDictionary] setValue:self
forKey:NSAssertionHandlerKey];
}
return self;
}
- (void)dealloc {
[[[NSThread currentThread] threadDictionary] removeObjectForKey:NSAssertionHandlerKey];
}
- (void)handleFailureInMethod:(SEL)selector object:(id)object file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ... {
if (self.handler) {
self.handler();
}
}
- (void)handleFailureInFunction:(NSString *)functionName file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ... {
if (self.handler) {
self.handler();
}
}
@end
Test.swift
let assertionHandler = SwiftAssertionHandler()
assertionHandler.handler = { () -> () in
// i.e. count number of assert
}
assert
y su precondition
hermano no lanzan excepciones no pueden ser "atrapados" (incluso con el manejo de errores de Swift 2).
Un truco que puede usar es escribir su propio reemplazo directo que haga lo mismo pero que se pueda reemplazar para las pruebas. (Si está preocupado por el rendimiento, simplemente #ifdef
it away para las versiones de lanzamiento).
condición previa personalizada
/// Our custom drop-in replacement `precondition`.
///
/// This will call Swift''s `precondition` by default (and terminate the program).
/// But it can be changed at runtime to be tested instead of terminating.
func precondition(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = "", file: StaticString = __FILE__, line: UWord = __LINE__) {
preconditionClosure(condition(), message(), file, line)
}
/// The actual function called by our custom `precondition`.
var preconditionClosure: (Bool, String, StaticString, UWord) -> () = defaultPreconditionClosure
let defaultPreconditionClosure = {Swift.precondition($0, $1, file: $2, line: $3)}
ayudante de prueba
import XCTest
extension XCTestCase {
func expectingPreconditionFailure(expectedMessage: String, @noescape block: () -> ()) {
let expectation = expectationWithDescription("failing precondition")
// Overwrite `precondition` with something that doesn''t terminate but verifies it happened.
preconditionClosure = {
(condition, message, file, line) in
if !condition {
expectation.fulfill()
XCTAssertEqual(message, expectedMessage, "precondition message didn''t match", file: file.stringValue, line: line)
}
}
// Call code.
block();
// Verify precondition "failed".
waitForExpectationsWithTimeout(0.0, handler: nil)
// Reset precondition.
preconditionClosure = defaultPreconditionClosure
}
}
ejemplo
func doSomething() {
precondition(false, "just not true")
}
class TestCase: XCTestCase {
func testExpectPreconditionFailure() {
expectingPreconditionFailure("just not true") {
doSomething();
}
}
}
( gist )
Código similar funcionará para assert
, por supuesto. Sin embargo, ya que está probando el comportamiento, obviamente desea que sea parte de su contrato de interfaz. No desea que el código optimizado lo viole, y la assert
se optimizará. Así que mejor usar la precondition
aquí.