unit test unit-testing swift xctest

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



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í.