Параметризованные модульные тесты в Swift

Есть ли способ использовать параметризованные модульные тесты, аналогичные тому, что вы можете достичь в .Net, используя NUnit структура.

[TestCase(12, 3, 4)]
[TestCase(12, 2, 6)]
[TestCase(12, 4, 3)]
public void DivideTest(int expectedResult, int a, int b)
{
  Assert.AreEqual(expectedResult, a / b);
}

Использование таких тестов (по сравнению с непараметризованными) может дать вам больше денег, позволяя избежать написания серий почти идентичных модульных тестов, отличающихся только значениями параметров.

Я ищу решение на основе XCTest или какие-либо другие средства для его достижения. Оптимальное решение должно сообщать о каждом тестовом примере (наборе параметров) как об отдельном модульном тесте в Xcode, поэтому ясно, все или только некоторые из тестовых случаев завершились неудачно.


person MDJ    schedule 16.02.2016    source источник
comment
Вы можете извлечь часть тестового метода во вспомогательный метод. Если вы умеете читать ObjC, это может помочь: qualitycoding.org/refactoring-tests   -  person dasdom    schedule 16.02.2016
comment
К сожалению, нам остается искать обходные пути, потому что XCTest до сих пор не имеет параметризованных тестов.   -  person Jon Reid    schedule 17.02.2016


Ответы (6)


Лучший способ использовать параметризованный — использовать свойство defaultTestSuite подкласса XCTestCase. Ярким примером с делением является следующий:

import XCTest

class ParameterizedExampleTests: XCTestCase {

    //properties to save the test cases
    private var array: [Float]? = nil
    private var expectedResult: Float? = nil

    // This makes the magic: defaultTestSuite has the set of all the test methods in the current runtime
    // so here we will create objects of ParameterizedExampleTests to call all the class' tests methodos
    // with differents values to test
    override open class var defaultTestSuite: XCTestSuite {
        let testSuite = XCTestSuite(name: NSStringFromClass(self))
        addTestsWithArray([12, 3], expectedResult: 4, toTestSuite: testSuite)
        addTestsWithArray([12, 2], expectedResult: 6, toTestSuite: testSuite)
        addTestsWithArray([12, 4], expectedResult: 3, toTestSuite: testSuite)
        return testSuite
    }

    // This is just to create the new ParameterizedExampleTests instance to add it into testSuite
    private class func addTestsWithArray(_ array: [Float], expectedResult: Float, toTestSuite testSuite: XCTestSuite) {
        testInvocations.forEach { invocation in
            let testCase = ParameterizedExampleTests(invocation: invocation)
            testCase.array = array
            testCase.expectedResult = expectedResult
            testSuite.addTest(testCase)
        }
    }

    // Normally this function is into production code (e.g. class, struct, etc).
    func division(a: Float, b: Float) -> Float {
        return a/b
    }

    func testDivision() {
        XCTAssertEqual(self.expectedResult, division(a: array?[0] ?? 0, b: array?[1] ?? 0))
    }
}
person DariusV    schedule 29.04.2018
comment
К сожалению, тесты набора отображаются только как один тест в навигаторе тестов Xcode. Есть ли работа для этого? - person tcurdt; 24.08.2019
comment
@tcurdt на самом деле это один запущенный тест, но проверенный несколько раз, поэтому он должен отображаться как один тест, но с несколькими вызовами, чтобы избежать создания ненужного дополнительного покрытия кода. - person DariusV; 27.08.2019
comment
Конечно, но невозможность детализировать результаты вызова значительно снижает полезность этого подхода. - person tcurdt; 27.08.2019

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

Учитывая эту функцию:

func multiply(_ a: Int, _ b: Int) -> Int {
    return a * b
}

У вас может быть несколько тестовых случаев:

class MyTest: XCTestCase {
    func testMultiply() {
        let cases = [(4,3,12), (2,4,8), (3,5,10), (4,6,20)]
        cases.forEach {
            XCTAssertEqual(multiply($0, $1), $2)
        }
    }
}

Последние два потерпят неудачу, и Xcode сообщит вам о них.

person Code Different    schedule 17.02.2016
comment
Основным недостатком этого является то, что выполнение останавливается, когда первый случай терпит неудачу. Там, где параметризованные тесты продолжаются, каждый случай рассматривается в отчетах как отдельный тест, что значительно упрощает определение того, какая именно комбинация не удалась. - person Kevin R; 10.10.2018
comment
@KevinR Вы когда-нибудь использовали фреймворк XCTest? Весь смысл использования XCTAssert и XCTAssertEqual вместо assert заключается в том, чтобы выполнение не останавливалось при возникновении сбоя. - person Peter Schorn; 02.09.2020
comment
Пожалуйста, никогда не используйте forEach. Это невероятно уродливо; просто используйте цикл for. - person Peter Schorn; 02.09.2020
comment
@PeterSchorn Выполнение тестовых случаев: да. Не выполнение этого конкретного теста. Если это больше не так, поведение могло быть изменено с течением времени. Также; не объявляйте свое личное мнение о стиле кода законом; никто не любит горьких людей. - person Kevin R; 04.09.2020
comment
Когда вы говорите об этом конкретном тесте, вы имеете в виду метод тестирования? В настоящее время выполнение никогда не останавливается, когда утверждение XC терпит неудачу, если только вы не установите continueAfterFailure в false, и в этом случае выполнение текущего тестового метода будет остановлено при возникновении ошибки. - person Peter Schorn; 04.09.2020

Мне больше нравится @DariusV решение. Однако это плохо работает, когда я, разработчик, запускаю тестовый метод непосредственно из боковой панели Xcode. Это нарушение сделки для меня.

То, что я закончил делать, я думаю, довольно гладко.

Я объявляю Dictionary из testValues (возможно, для этого нужно лучшее имя) как вычисляемое свойство экземпляра моего подкласса XCTestCase. Затем я определяю Dictionary литерал входных данных, обозначающих ожидаемые выходные данные. В моем примере проверяется функция, которая воздействует на Int, поэтому я определяю testValues так:

static var testRange: ClosedRange<Int> { 0...100 }

var testValues: [Int: Int] {
    let range = Self.testRange
    return [
        // Lower extreme
        Int.min: range.lowerBound,

        // Lower bound
        range.lowerBound - 1: range.lowerBound,
        range.lowerBound    : range.lowerBound,
        range.lowerBound + 1: range.lowerBound + 1,

        // Middle
        25: 25,
        50: 50,
        75: 75,

        // Upper bound
        range.upperBound - 1: range.upperBound - 1,
        range.upperBound    : range.upperBound,
        range.upperBound + 1: range.upperBound,

        // Upper extreme
        Int.max: range.upperBound
    ]
}

Здесь я очень легко объявляю свои крайние и граничные случаи. Более семантический способ сделать то же самое может заключаться в использовании массива кортежей, но литеральный синтаксис словаря Swift достаточно тонкий, и я знаю, что он делает. ????

Теперь в моем тестовом методе есть простой цикл for.

/// The property wrapper I'm testing. This could be anything, but this works for example.
@Clamped(testRange) var number = 50

func testClamping() {
    let initial = number

    for (input, expected) in testValues {
        // Call the code I'm testing. (Computed properties act on assignment)
        number = input
        XCTAssertEqual(number, expected, "{number = \(input)}: `number` should be \(expected)")

        // Reset after each iteration.
        number = initial
    }
}

Теперь, чтобы запустить для каждого параметра, я просто вызываю XCTests любым из обычных способов Xcode или любым другим способом, который работает с Linux (я полагаю). Нет необходимости запускать каждый тестовый класс, чтобы получить параметризованность этого.

Разве это не красиво? Я только что рассмотрел каждое граничное значение и класс эквивалентности всего в нескольких строках кода DRY!

Что касается определения неудачных случаев, каждый вызов проходит через функцию XCTAssert, которая, согласно соглашению Xcode, будет выдавать вам сообщение только в том случае, если что-то не так, о чем вам нужно подумать. Они отображаются на боковой панели, но похожие сообщения имеют тенденцию смешиваться друг с другом. Моя строка сообщения здесь идентифицирует ошибочный ввод и его результирующий вывод, исправляя смешивание вместе и превращая мой поток тестирования в нормальный кусок вишневого яблочного пирога. (Ты можешь отформатировать свой как угодно, деревенщина! Все, что благословит твое здравомыслие.)

Вкусные.

TL;DR

Адаптация @Code Different ответ: используйте словарь входных и выходных данных и выполните цикл for. ????

person AverageHelper    schedule 10.02.2020
comment
Зачем вам нужно брать один дополнительный объект для поддержания текущего индекса, когда вы можете получить индекс, просто перечислив его в цикле? - person nikhil nangia; 19.05.2020
comment
@nikhilngia Я ... не поддерживаю текущий индекс. Я не уверен, что ты имеешь в виду; цикл дает мне текущий ключ и значение в словаре. Я вызываю свой код, используя ключ для ввода и сравнивая вывод со значением, которое является ожидаемым результатом. Значение initial устанавливается перед циклом, чтобы я мог сбросить состояние тестируемого устройства. Я не вижу здесь никакой индексной переменной. - person AverageHelper; 22.05.2020
comment
Почему testRange является вычисляемым свойством? Это совершенно не нужно. Просто используйте сохраненное свойство. Также testValues не обязательно должно быть вычисляемым свойством. Оба свойства всегда возвращают одно и то же значение. - person Peter Schorn; 04.09.2020
comment
@PeterSchorn Ты прав. static let должно отлично работать для этого. - person AverageHelper; 04.09.2020
comment
@SeizeTheDay Рад, что смог помочь улучшить ваш код. Кроме того, вам следует по возможности избегать использования свойств экземпляра в ваших тестовых классах. Хотите верьте, хотите нет, но XCTest фактически создает отдельный экземпляр вашего тестового класса для каждого метода тестирования. См. stackoverflow.com/a/48562936/12394554. Поэтому вместо этого вы должны использовать свойства static/class. - person Peter Schorn; 05.09.2020

Ответ @Code Different является законным. Вот еще два варианта или, скорее, обходные пути:

Тестирование на основе свойств

Вы можете использовать такой инструмент, как Fox, для выполнения генеративного тестирования, где тест Framework сгенерирует множество входных наборов для поведения, которое вы хотите протестировать, и запустит их для вас.

Подробнее об этом подходе:

Общие примеры BDD

Если вам нравится стиль BDD и вы используете среду тестирования, которая их поддерживает, вы можете используйте общие примеры.

При использовании Quick это будет выглядеть так:

class MultiplySharedExamplesConfiguration: QuickConfiguration {
  override class func configure(configuration: Configuration) {
    sharedExamples("something edible") { (sharedExampleContext: SharedExampleContext) in
      it("multiplies two numbers") {
        let a = sharedExampleContext()["a"]
        let b = sharedExampleContext()["b"]
        let expectedResult = sharedExampleContext()["b"]

        expect(multiply(a, b)) == expectedResult
      }
    }
  }
}

class MultiplicationSpec: QuickSpec {
  override func spec() {
    itBehavesLike("it multiplies two numbers") { ["a": 2, "b": 3, "result": 6] }
    itBehavesLike("it multiplies two numbers") { ["a": 2, "b": 4, "result": 8] }
    itBehavesLike("it multiplies two numbers") { ["a": 3, "b": 3, "result": 9] }
  }
}

Честно говоря, этот вариант: 1) много работы, 2) неправильное использование техники общего примера, поскольку вы используете их не для проверки поведения, общего для нескольких классов, а скорее для параметризации теста. Но, как я сказал в начале, это скорее обходной путь.

person mokagio    schedule 19.02.2016
comment
Мне нравится общая идея генеративного тестирования. Возможно, его можно использовать в сочетании со средой выполнения ObjC для параметризации тестов. Определенно хороший источник вдохновения. - person MDJ; 21.02.2016

Все утверждения кажутся throw, поэтому, возможно, вам подойдет что-то вроде этого:

typealias Division = (dividend: Int, divisor: Int, quotient: Int)

func testDivisions() {
    XCTAssertNoThrow(try divisionTest((12, 3, 4)))
    XCTAssertNoThrow(try divisionTest((12, 2, 6)))
    XCTAssertNoThrow(try divisionTest((12, 4, 3)))
}

private func divisionTest(_ division: Division) throws {
    XCTAssertEqual(division.dividend / division.divisor, division.quotient)
}

Если один выйдет из строя, вся функция выйдет из строя. Если требуется большая степень детализации, каждый случай можно разделить на отдельную функцию.

person zath    schedule 27.08.2019

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

Другой вариант — проверить XCTestPlan в XCode. Об этом есть информативное видео с WWDC.

person Nick O    schedule 16.10.2019