Как динамически добавить XCTestCase - PullRequest
3 голосов
/ 13 марта 2019

Я пишу UI Test для проекта белого ярлыка, где у каждого приложения есть свой набор пунктов меню. Тест нажимает на каждый пункт меню и делает снимок экрана (используя снимок fastlane ).

В настоящее время все это происходит внутри одного 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()          ✓

Я посмотрел документацию по программному созданию тестов , но я не уверен, как быстро это настроить. - В идеале я бы использовал замыкания, чтобы обернуть существующие тесты скриншотов в их собственные XCTestCase - Я представил это, как показано ниже, но, похоже, нет никаких полезных методов инициализации, чтобы это произошло:

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

Я не понимаю комбинацию вызовов и селекторов, которые предлагается использовать в документации, и я не могу найти хороших примеров, пожалуйста, укажите мне правильное направление или поделитесь любыми примерами этой работы.

1 Ответ

1 голос
/ 17 марта 2019

Вы, вероятно, не можете сделать это чисто быстро, так как NSInvocation больше не является частью быстрого API.

XCTest полагается на функцию + (NSArray<NSInvocation *> *)testInvocations для получения списка методов тестирования внутри одного XCTestCase класса.Реализация по умолчанию, как вы можете предположить, просто найти все методы, которые начинаются с префикса test, и вернуть их, завернутые в NSInvocation.(Вы можете прочитать больше о NSInvocation здесь )
Поэтому, если мы хотим, чтобы тесты объявлялись во время выполнения, это нас интересует.
К сожалению NSInvocation не является частьюswift api больше, и мы не можем переопределить этот метод.

Если вы можете использовать немного ObjC, тогда мы можем создать суперкласс, который скрывает детали NSInvocation внутри и предоставляет дружественный для swift 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

Так что теперь наши классы быстрых тестов должны просто наследовать от этого класса и переопределять _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.

Слава команде Quick для реализации суперкласса .

Редактировать : Я создал репо с примером github

...