如何动态添加 XCTestCase

Wez*_*Wez 5 ios swift xctestcase xcuitest

我正在为一个白标项目编写 UI 测试,其中每个应用程序都有一组不同的菜单项。测试点击每个菜单项并截取屏幕截图(使用fastlane snapshot)。

目前这一切都发生在一个XCTestCase叫做的testScreenshotAllMenuItems()里面,看起来像这样:

func testScreenshotAllMenuItems() {
    // Take a screenshot of the menu
    openTheMenu()
    snapshot("Menu")
    var cells:[XCUIElement] = []

    // Store each menu item for use later
    for i in 0..<app.tables.cells.count {
        cells.append(app.tables.cells.element(boundBy: i))
    }

    // Loop through each menu item
    for menuItem in cells.enumerated() {
        let exists = menuItem.element.waitForExistence(timeout: 5)
        if exists && menuItem.element.isHittable {
            // Only tap on the menu item if it isn't an external link
            let externalLink = menuItem.element.children(matching: .image)["external link"]
            if !externalLink.exists {
                var name = "\(menuItem.offset)"
                let cellText = menuItem.element.children(matching: .staticText).firstMatch
                if cellText.label != "" {
                    name += "-\(cellText.label.replacingOccurrences(of: " ", with: "-"))"
                }
                print("opening \(name)")
                menuItem.element.tap()
                // Screenshot this view and then re-open the menu
                snapshot(name)
                openTheMenu()
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我希望能够动态生成每个屏幕截图,因为它是自己的测试用例,以便这些将作为单独的测试正确报告,可能类似于:

[T] Screenshots
    [t] testFavouritesViewScreenShot()        ?
    [t] testGiveFeedbackViewScreenShot()      ?
    [t] testSettingsViewScreenShot()          ?
Run Code Online (Sandbox Code Playgroud)

我已经查看了有关以编程方式创建测试的文档,但我不确定如何以快速的方式进行设置。- 理想情况下,我会使用闭包将现有的屏幕截图测试包装到他们自己的XCTestCase- 我想象如下,但似乎没有任何有用的 init 方法来实现这一点:

for menuItem in cells {
    let test = XCTestCase(closure: {
        menuItem.tap()
        snapshot("menuItemName")
    })
    test.run()
}
Run Code Online (Sandbox Code Playgroud)

我不明白文档建议使用的调用和选择器的组合,我找不到任何好的例子,请指出正确的方向,或者分享你在这个工作中的任何例子。

Man*_*ear 6

您可能无法在纯粹的 swift 中做到这一点,因为NSInvocation它不再是 swift api 的一部分。

XCTest 依靠+ (NSArray<NSInvocation *> *)testInvocations函数来获取一个XCTestCase类中的测试方法列表。您可以假设的默认实现只是找到所有以test前缀开头的方法并将它们返回到NSInvocation. (您可以NSInvocation 在此处阅读更多相关信息)
因此,如果我们想在运行时声明测试,这就是我们感兴趣的地方。
不幸的NSInvocation是,它不再是 swift api 的一部分,我们无法覆盖此方法。

如果您可以使用一点 ObjC,那么我们可以创建隐藏 NSInvocation 详细信息的超类,并为子类提供快速友好的 api。

/// Parent.h

/// SEL is just pointer on C struct so we cannot put it inside of NSArray.  
/// Instead we use this class as wrapper.
@interface _QuickSelectorWrapper : NSObject
- (instancetype)initWithSelector:(SEL)selector;
@end

@interface ParametrizedTestCase : XCTestCase
/// List of test methods to call. By default return nothing
+ (NSArray<_QuickSelectorWrapper *> *)_qck_testMethodSelectors;
@end
Run Code Online (Sandbox Code Playgroud)
/// Parent.m

#include "Parent.h"
@interface _QuickSelectorWrapper ()
@property(nonatomic, assign) SEL selector;
@end

@implementation _QuickSelectorWrapper
- (instancetype)initWithSelector:(SEL)selector {
    self = [super init];
    _selector = selector;
    return self;
}
@end

@implementation ParametrizedTestCase
+ (NSArray<NSInvocation *> *)testInvocations {
    // here we take list of test selectors from subclass
    NSArray<_QuickSelectorWrapper *> *wrappers = [self _qck_testMethodSelectors];
    NSMutableArray<NSInvocation *> *invocations = [NSMutableArray arrayWithCapacity:wrappers.count];

    // And wrap them in NSInvocation as XCTest api require
    for (_QuickSelectorWrapper *wrapper in wrappers) {
        SEL selector = wrapper.selector;
        NSMethodSignature *signature = [self instanceMethodSignatureForSelector:selector];
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
        invocation.selector = selector;

        [invocations addObject:invocation];
    }

    /// If you want to mix parametrized test with normal `test_something` then you need to call super and append his invocations as well.
    /// Otherwise `test`-prefixed methods will be ignored
    return invocations;
}

+ (NSArray<_QuickSelectorWrapper *> *)_qck_testMethodSelectors {
    return @[];
}
@end
Run Code Online (Sandbox Code Playgroud)

所以现在我们的 swift 测试类只需要从这个类继承并覆盖_qck_testMethodSelectors

/// Parent.h

/// SEL is just pointer on C struct so we cannot put it inside of NSArray.  
/// Instead we use this class as wrapper.
@interface _QuickSelectorWrapper : NSObject
- (instancetype)initWithSelector:(SEL)selector;
@end

@interface ParametrizedTestCase : XCTestCase
/// List of test methods to call. By default return nothing
+ (NSArray<_QuickSelectorWrapper *> *)_qck_testMethodSelectors;
@end
Run Code Online (Sandbox Code Playgroud)

预期输出:

Test Suite 'RuntimeTests' started at 2019-03-17 06:09:24.150
Test Case '-[ProtocolUnitTests.RuntimeTests test_a]' started.
Magic: a
Test Case '-[ProtocolUnitTests.RuntimeTests test_a]' passed (0.006 seconds).
Test Case '-[ProtocolUnitTests.RuntimeTests test_b]' started.
Magic: b
Test Case '-[ProtocolUnitTests.RuntimeTests test_b]' passed (0.001 seconds).
Test Case '-[ProtocolUnitTests.RuntimeTests test_c]' started.
Magic: c
Test Case '-[ProtocolUnitTests.RuntimeTests test_c]' passed (0.001 seconds).
Test Suite 'RuntimeTests' passed at 2019-03-17 06:09:24.159.
Run Code Online (Sandbox Code Playgroud)

感谢 Quick 团队实现超类

编辑:我用示例github创建了 repo