针对不同接口实现的共享 XCTest 单元测试
Shared XCTest unit tests for different implementations of interface
我有一些接口(协议)的两个或多个实现:
protocol Interface {
func methodOne()
func methodTwo()
}
我想测试每个实现,我不想重复代码。我有几个选择,但 none 个让我满意。
第一个是为 ImplementationA
创建测试用例并将其子类化以获得 ImplementationB
:
的测试用例
class ImplementationATests: XCTestCase {
var implToTest: Interface!
override func setUp() {
super.setUp()
implToTest = ImplementationA()
}
func testMethodOne() {
...
}
func testMethodTwo() {
...
}
}
class ImplementationBTests: ImplementationATests {
override func setUp() {
super.setUp()
implToTest = ImplementationB()
}
}
此方法的缺点之一是我无法进行仅适用于 ImplementationA
的测试。 (例如,测试特定于该实现的一些辅助方法)
我想到的第二个选项是为测试用例创建共享子类:
class InterfaceTests: XCTestCase {
var implToTest: Interface!
func testMethodOne() {
...
}
func testMethodTwo() {
...
}
}
但是这里也会执行那些测试,但它们会失败,因为没有为 implToTest
分配实现。当然我可以为它分配一些实现,但是我将以相同实现的两个测试用例结束。最好的选择是以某种方式禁用 InterfaceTests
测试用例和 运行 仅它的子类。可能吗?
我想到的第三个想法可能看起来很棘手,但它可以满足我的所有需求。不幸的是它不起作用。
我决定创建 InterfaceTestable
协议:
protocol InterfaceTestable {
var implToTest: Interface! { get set }
}
并使用所有共享测试对其进行扩展:
extension InterfaceTestable {
func testMethodOne() {
...
}
func testMethodTwo() {
...
}
}
然后为每个实现创建测试用例:
class ImplementationATests: XCTestCase, InterfaceTestable {
var implToTest: Interface!
override func setUp() {
super.setUp()
implToTest = ImplementationA()
}
// some tests which only apply to ImplementationA
}
class ImplementationBTests: XCTestCase, InterfaceTestable {
var implToTest: Interface!
override func setUp() {
super.setUp()
implToTest = ImplementationB()
}
// some tests which only apply to ImplementationB
}
那些测试用例编译但 Xcode 没有看到在 InterfaceTestable
扩展中声明的测试。
有没有其他方法可以为不同的实现共享测试?
我之前使用的方法是共享基础 class。使 implToTest
可空。在基础 class 中,如果未提供实现,只需 return
在保护子句中退出测试。
有点烦人的是,测试 运行 包含基础 class 测试的报告,但它什么也没做。但这是一个小烦恼。测试子classes 将提供有用的反馈。
我遇到了同样的问题,并使用你的第二个选项解决了它。
但是,我找到了一种方法来防止 class 中的测试用例来自 运行ning:
将基础 class 中的 defaultTestSuite()
class 方法覆盖为 return 空 XCTestSuite
:
class InterfaceTests: XCTestCase {
var implToTest: Interface!
override class func defaultTestSuite() -> XCTestSuite {
return XCTestSuite(name: "InterfaceTests Excluded")
}
}
有了这个,InterfaceTests
的测试就没有 运行。不幸的是,也没有 ImplementationATests
的测试。通过覆盖 ImplementationATests
中的 defaultTestSuite()
这可以解决:
class ImplementationATests : XCTestCase {
override func setUp() {
super.setUp()
implToTest = ImplementationA()
}
override class func defaultTestSuite() -> XCTestSuite {
return XCTestSuite(forTestCaseClass: ImplementationATests.self)
}
}
现在 ImplementationATests
的测试套件将 运行 所有来自 InterfaceTests
的测试,但是来自 InterfaceTests
的测试没有直接 运行,没有设置 implToTest
.
在 ithron 的解决方案之上构建,如果您精心设计 defaultTestSuite
,则可以消除对每个子 class 到 re-override 的需要。
class InterfaceTests: XCTestCase {
override class var defaultTestSuite: XCTestSuite {
// When subclasses inherit this property, they'll fail this check, and hit the `else`.
// At which point, they'll inherit the full test suite that generated for them.
if self == AbstractTest.self {
return XCTestSuite(name: "Empty suite for AbstractSpec")
} else {
return super.defaultTestSuite
}
}
}
当然,同样的限制适用:这不会从 Xcode 测试导航器中隐藏空测试套件。
将其概括为 AbstractTestCase
class
我会更进一步,创建一个基础 class AbstractTestCase: XCTestCase
来存储这个 defaultTestSuite
技巧,您的所有其他抽象 classes 都可以从中继承。
为了完整起见,要使其真正抽象,您还需要覆盖所有 XCTestCase
初始化程序,以便在尝试实例化您的抽象 classes 时使它们出错。在 Apple 的平台上,有 3 个初始化器要覆盖:
-[XCTestCase init]
-[XCTestCase initWithSelector:]
-[XCTestCase initWithInvocation:]
不幸的是,这不能从 Swift 完成,因为最后一个初始化程序使用 NSInvocation
。 NSInvocation
不适用于 Swift(它与 Swift 的 ARC 不兼容)。所以你需要在 Objective C 中实现它。这是我的尝试:
AbstractTestCase.h
#import <XCTest/XCTest.h>
@interface AbstractTestCase : XCTestCase
@end
AbstractTestCase.m
#import "AbstractTestCase.h"
@implementation AbstractTestCase
+ (XCTestSuite *)defaultTestSuite {
if (self == [AbstractTestCase class]) {
return [[XCTestSuite alloc] initWithName: @"Empty suite for AbstractTestCase"];
} else {
return [super defaultTestSuite];
}
}
- (instancetype)init {
self = [super init];
NSAssert(![self isMemberOfClass:[AbstractTestCase class]], @"Do not instantiate this abstract class!");
return self;
}
- (instancetype)initWithSelector:(SEL)selector {
self = [super initWithSelector:selector];
NSAssert(![self isMemberOfClass:[AbstractTestCase class]], @"Do not instantiate this abstract class!");
return self;
}
- (instancetype)initWithInvocation:(NSInvocation *)invocation {
self = [super initWithInvocation:invocation];
NSAssert(![self isMemberOfClass:[AbstractTestCase class]], @"Do not instantiate this abstract class!");
return self;
}
@end
用法
然后您可以将其用作抽象测试的超级class,例如
class InterfaceTests: AbstractTestCase {
var implToTest: Interface!
func testSharedTest() {
}
}
我有一些接口(协议)的两个或多个实现:
protocol Interface {
func methodOne()
func methodTwo()
}
我想测试每个实现,我不想重复代码。我有几个选择,但 none 个让我满意。
第一个是为 ImplementationA
创建测试用例并将其子类化以获得 ImplementationB
:
class ImplementationATests: XCTestCase {
var implToTest: Interface!
override func setUp() {
super.setUp()
implToTest = ImplementationA()
}
func testMethodOne() {
...
}
func testMethodTwo() {
...
}
}
class ImplementationBTests: ImplementationATests {
override func setUp() {
super.setUp()
implToTest = ImplementationB()
}
}
此方法的缺点之一是我无法进行仅适用于 ImplementationA
的测试。 (例如,测试特定于该实现的一些辅助方法)
我想到的第二个选项是为测试用例创建共享子类:
class InterfaceTests: XCTestCase {
var implToTest: Interface!
func testMethodOne() {
...
}
func testMethodTwo() {
...
}
}
但是这里也会执行那些测试,但它们会失败,因为没有为 implToTest
分配实现。当然我可以为它分配一些实现,但是我将以相同实现的两个测试用例结束。最好的选择是以某种方式禁用 InterfaceTests
测试用例和 运行 仅它的子类。可能吗?
我想到的第三个想法可能看起来很棘手,但它可以满足我的所有需求。不幸的是它不起作用。
我决定创建 InterfaceTestable
协议:
protocol InterfaceTestable {
var implToTest: Interface! { get set }
}
并使用所有共享测试对其进行扩展:
extension InterfaceTestable {
func testMethodOne() {
...
}
func testMethodTwo() {
...
}
}
然后为每个实现创建测试用例:
class ImplementationATests: XCTestCase, InterfaceTestable {
var implToTest: Interface!
override func setUp() {
super.setUp()
implToTest = ImplementationA()
}
// some tests which only apply to ImplementationA
}
class ImplementationBTests: XCTestCase, InterfaceTestable {
var implToTest: Interface!
override func setUp() {
super.setUp()
implToTest = ImplementationB()
}
// some tests which only apply to ImplementationB
}
那些测试用例编译但 Xcode 没有看到在 InterfaceTestable
扩展中声明的测试。
有没有其他方法可以为不同的实现共享测试?
我之前使用的方法是共享基础 class。使 implToTest
可空。在基础 class 中,如果未提供实现,只需 return
在保护子句中退出测试。
有点烦人的是,测试 运行 包含基础 class 测试的报告,但它什么也没做。但这是一个小烦恼。测试子classes 将提供有用的反馈。
我遇到了同样的问题,并使用你的第二个选项解决了它。 但是,我找到了一种方法来防止 class 中的测试用例来自 运行ning:
将基础 class 中的 defaultTestSuite()
class 方法覆盖为 return 空 XCTestSuite
:
class InterfaceTests: XCTestCase {
var implToTest: Interface!
override class func defaultTestSuite() -> XCTestSuite {
return XCTestSuite(name: "InterfaceTests Excluded")
}
}
有了这个,InterfaceTests
的测试就没有 运行。不幸的是,也没有 ImplementationATests
的测试。通过覆盖 ImplementationATests
中的 defaultTestSuite()
这可以解决:
class ImplementationATests : XCTestCase {
override func setUp() {
super.setUp()
implToTest = ImplementationA()
}
override class func defaultTestSuite() -> XCTestSuite {
return XCTestSuite(forTestCaseClass: ImplementationATests.self)
}
}
现在 ImplementationATests
的测试套件将 运行 所有来自 InterfaceTests
的测试,但是来自 InterfaceTests
的测试没有直接 运行,没有设置 implToTest
.
在 ithron 的解决方案之上构建,如果您精心设计 defaultTestSuite
,则可以消除对每个子 class 到 re-override 的需要。
class InterfaceTests: XCTestCase {
override class var defaultTestSuite: XCTestSuite {
// When subclasses inherit this property, they'll fail this check, and hit the `else`.
// At which point, they'll inherit the full test suite that generated for them.
if self == AbstractTest.self {
return XCTestSuite(name: "Empty suite for AbstractSpec")
} else {
return super.defaultTestSuite
}
}
}
当然,同样的限制适用:这不会从 Xcode 测试导航器中隐藏空测试套件。
将其概括为 AbstractTestCase
class
我会更进一步,创建一个基础 class AbstractTestCase: XCTestCase
来存储这个 defaultTestSuite
技巧,您的所有其他抽象 classes 都可以从中继承。
为了完整起见,要使其真正抽象,您还需要覆盖所有 XCTestCase
初始化程序,以便在尝试实例化您的抽象 classes 时使它们出错。在 Apple 的平台上,有 3 个初始化器要覆盖:
-[XCTestCase init]
-[XCTestCase initWithSelector:]
-[XCTestCase initWithInvocation:]
不幸的是,这不能从 Swift 完成,因为最后一个初始化程序使用 NSInvocation
。 NSInvocation
不适用于 Swift(它与 Swift 的 ARC 不兼容)。所以你需要在 Objective C 中实现它。这是我的尝试:
AbstractTestCase.h
#import <XCTest/XCTest.h>
@interface AbstractTestCase : XCTestCase
@end
AbstractTestCase.m
#import "AbstractTestCase.h"
@implementation AbstractTestCase
+ (XCTestSuite *)defaultTestSuite {
if (self == [AbstractTestCase class]) {
return [[XCTestSuite alloc] initWithName: @"Empty suite for AbstractTestCase"];
} else {
return [super defaultTestSuite];
}
}
- (instancetype)init {
self = [super init];
NSAssert(![self isMemberOfClass:[AbstractTestCase class]], @"Do not instantiate this abstract class!");
return self;
}
- (instancetype)initWithSelector:(SEL)selector {
self = [super initWithSelector:selector];
NSAssert(![self isMemberOfClass:[AbstractTestCase class]], @"Do not instantiate this abstract class!");
return self;
}
- (instancetype)initWithInvocation:(NSInvocation *)invocation {
self = [super initWithInvocation:invocation];
NSAssert(![self isMemberOfClass:[AbstractTestCase class]], @"Do not instantiate this abstract class!");
return self;
}
@end
用法
然后您可以将其用作抽象测试的超级class,例如
class InterfaceTests: AbstractTestCase {
var implToTest: Interface!
func testSharedTest() {
}
}