代码之家  ›  专栏  ›  技术社区  ›  Wez

如何动态添加XCTestCase

  •  0
  • Wez  · 技术社区  · 5 年前

    我正在为一个白标签项目编写一个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()
                }
            }
        }
    }
    

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

    [T] Screenshots
        [t] testFavouritesViewScreenShot()        ✓
        [t] testGiveFeedbackViewScreenShot()      ✓
        [t] testSettingsViewScreenShot()          ✓
    

    我已经看过了关于 creating tests programmatically 但我不知道如何快速设置。-理想情况下,我会使用闭包将现有的屏幕截图测试打包到自己的测试中 XCTestCase -我的设想是这样的,但似乎没有任何有用的init方法来实现这一点:

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

    我不理解文档中建议使用的调用和选择器的组合,我也找不到任何好的例子,请给我指出正确的方向,或者与我分享你在这方面的任何例子。

    0 回复  |  直到 5 年前
        1
  •  6
  •   ManWithBear    5 年前

    你可能不能用纯斯威夫特 NSInvocation 不再是swift api的一部分。

    依赖 + (NSArray<NSInvocation *> *)testInvocations 函数以获取其中的测试方法列表 XCTestCase 班默认实现,正如您可以假设的那样,只需找到以 test 添加前缀并将其包装返回 NSInvocation (你可以读更多关于 NSInvocation here
    所以,如果我们想在运行时声明测试,这是我们感兴趣的一点。
    不幸地 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
    
    /// 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
    

    所以现在我们的swift测试类需要从这个类继承并重写 _qck_testMethodSelectors :

    /// RuntimeTests.swift
    
    class RuntimeTests: ParametrizedTestCase {
    
        /// This is our parametrized method. For this example it just print out parameter value
        func p(_ s: String) {
            print("Magic: \(s)")
        }
    
        override class func _qck_testMethodSelectors() -> [_QuickSelectorWrapper] {
            /// For this example we create 3 runtime tests "test_a", "test_b" and "test_c" with corresponding parameter
            return ["a", "b", "c"].map { parameter in
                /// first we wrap our test method in block that takes TestCase instance
                let block: @convention(block) (RuntimeTests) -> Void = { $0.p(parameter) }
                /// with help of ObjC runtime we add new test method to class
                let implementation = imp_implementationWithBlock(block)
                let selectorName = "test_\(parameter)"
                let selector = NSSelectorFromString(selectorName)
                class_addMethod(self, selector, implementation, "v@:")
                /// and return wrapped selector on new created method
                return _QuickSelectorWrapper(selector: selector)
            }
        }
    }
    

    预期产出:

    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.
    

    快速团队的荣誉 super class implementation .

    编辑 :我用例子创建了回购协议 github