Проверка утверждения в Swift
Я пишу модульные тесты для метода, который имеет утверждение. Руководство Swift Language рекомендует использовать утверждения для "недопустимых условий":
Утверждения приводят к прекращению работы вашего приложения и не заменяют разработку кода таким образом, что вряд ли возникнут недопустимые условия. Тем не менее, в ситуациях, когда возможны недопустимые условия, утверждение является эффективным способом обеспечения того, чтобы такие условия были выделены и замечены во время разработки, прежде чем ваше приложение будет опубликовано.
Я хочу проверить случай неудачи.
Тем не менее, нет XCTAssertThrows
в Свифте (по состоянию на бета 6). Как я могу написать модульный тест, который проверяет, что утверждение не выполняется?
редактировать
Согласно предложению @RobNapier, я попробовал обернуть XCTAssertThrows
в методе Objective-C и вызове этого метода из Swift. Это не работает, так как макрос не улавливает фатальную ошибку, вызванную assert
и, следовательно, тест вылетает.
6 ответов
assert
и его брат precondition
не выбрасывать исключения нельзя "поймать" (даже с обработкой ошибок Swift 2).
Уловка, которую вы можете использовать, - написать собственную замену, которая делает то же самое, но может быть заменена для тестов. (Если вы беспокоитесь о производительности, просто #ifdef
это для релизных сборок.)
таможенное условие
/// 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)}
помощник по тестированию
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
}
}
пример
func doSomething() {
precondition(false, "just not true")
}
class TestCase: XCTestCase {
func testExpectPreconditionFailure() {
expectingPreconditionFailure("just not true") {
doSomething();
}
}
}
( суть)
Подобный код будет работать для assert
, конечно. Однако, поскольку вы тестируете поведение, вы, очевидно, хотите, чтобы оно было частью вашего контракта на интерфейс. Вы не хотите, чтобы оптимизированный код нарушал его, и assert
будет оптимизирован далеко. Так лучше использовать precondition
Вот.
Согласитесь с комментарием nschum, что он не подходит для юнит-теста assert
потому что по умолчанию это не будет в коде продукта. Но если вы действительно хотели это сделать, вот assert
версия для справки:
переопределить утверждать
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)}
вспомогательное расширение
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
}
}
Спасибо nschum и Ken Ko за идею этого ответа.
Вот суть того, как это сделать
Этот ответ не только для утверждения. Это также для других методов утверждения (assert
, assertionFailure
, precondition
, preconditionFailure
а также fatalError
)
1. падение ProgrammerAssertions.swift
к цели вашего приложения или тестируемой среды. Просто помимо вашего исходного кода.
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
к вашей цели теста. Просто помимо ваших тестовых случаев.
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. Используйте assert
, assertionFailure
, precondition
, preconditionFailure
а также fatalError
как обычно, как всегда.
Например: если у вас есть функция, которая выполняет деление, подобное следующему:
func divideFatalError(x: Float, by y: Float) -> Float {
guard y != 0 else {
fatalError("Zero division")
}
return x / y
}
4. Проведите их тестирование новыми методами expectAssert
, expectAssertionFailure
, expectPrecondition
, expectPreconditionFailure
а также expectFatalError
,
Вы можете проверить деление 0 с помощью следующего кода.
func testFatalCorrectMessage() {
expectFatalError("Zero division") {
divideFatalError(1, by: 0)
}
}
Или, если вы не хотите проверять сообщение, вы просто делаете.
func testFatalErrorNoMessage() {
expectFatalError() {
divideFatalError(1, by: 0)
}
}
Проект Мэтта Галлахера CwlPreconditionTesting на github добавляет catchBadInstruction
функция, которая дает вам возможность проверить на наличие ошибок утверждения / предварительного условия в коде модульного тестирования.
Файл CwlCatchBadInstructionTests показывает простую иллюстрацию его использования. (Обратите внимание, что он работает только в симуляторе для iOS.)
Я полагаю, что с Beta6 для Swift все еще невозможно поймать исключение напрямую. Единственный способ справиться с этим - написать конкретный контрольный пример в ObjC.
Тем не менее, обратите внимание, что _XCTAssertionType.Throws
существует, что говорит о том, что команда Swift знает об этом и собирается в конечном итоге найти решение. Вполне возможно, что вы могли бы написать это утверждение в ObjC и представить его Свифту (я не могу придумать ни одной причины, которая была бы невозможна в Beta6). Одна большая проблема заключается в том, что вы не сможете легко получить из него точную информацию о местоположении (например, конкретную строку, которая не удалась).
У нас есть код Swift (4), который тестирует среду Objective-C. Некоторые из базовых методов вызывают в NSAssert
,
Вдохновленный NSHipster, я получил такую реализацию:
SwiftAssertionHandler.h (используйте это в мостовом заголовке)
@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
}