@Suite

Глава про макрос @Suite разделена на 4 сценария:

  1. Объединение в группу
  2. Настройка отображаемого имени
  3. Поддержка методов
  4. Ограничения и тонкости использования

Объединение в группу

Что приходит на ум при работе с большим количеством функций? Правильно, возможность упорядочить их с помощью чего-либо, а ещё лучше объединить их в один тип данных. Макрос @Suite идеально подходит не только для объединения в один тип данных, а так же для использования трейтов.

Вообще, такая группировка возможна в 2ух сценариях:

  • С помощью пользовательских типов данных
  • Применяя атрибут @Suite для пользовательских типов данных

К таким типам данных относят: struct, class, actor и enum.

@Suite
struct SingleProfile {...}

@Suite
class SharedService {...}

@Suite
actor DatabaseProvider {...}

@Suite
enum SwaggerAPI {...}

tip

@Suite не является обязательным атрибутом для применения, но позволяет использовать трейты, подобные в макросе @Test, для расширения функционала.

Атрибут @Suite не является обязательным для распознавания тестовых функций, содержащихся в типе, но его использование позволяет настраивать отображение типа данных в IDE и в командной строке. Если к типу данных применён трейт, такой как .tags(...) или .disabled(...), он автоматически наследуется всеми тестами, содержащимися в этом типе данных.

Пользовательские типы данных могут содержать не только функции для тестирования, но и другие методы. Также они могут содержать вложенные типы данных. Чтобы добавить вложенный тип данных, нужно объявить дополнительный тип внутри основного.

По умолчанию тесты в рамках одного типа данных выполняются параллельно. Подробнее о параллелизации можно узнать в разделе Конкурентный и последовательный запуск тестов.

Настройка имени данным

Чтобы задать имя для типа данных, передайте строковый литерал в качестве аргумента для атрибута @Suite:

@Suite("Food truck tests")
struct FoodTruckTests {
    @Test func foodTruckExists() {
        // ... логика
    }
}

Для дальнейшей настройки внешнего вида и поведения метода можно использовать трейты, такие как .tags(...).

Методы для тестирования в Suite

Если метод для тестирования объявлен как метод экземпляра (без использования ключевых слов static или class), библиотека тестирования вызывает этот обычный метод (без атрибута @Test), а затем вызывает метод на этом экземпляре. Например:

@Suite
struct ColdWeather {
    @Test
	func dressWarm() {
		#expect(-10 + -3 == -13)
	}
}

Эквивалентно следующему:

@Suite
struct ColdWeather {
	func dressWarm() {
		#expect(-10 + -3 == -13)
	}

	@Test
	static func someStaticFn() {
		let instance = ColdWeather()
		instance.dressWarm()
	}
}

В консоли Xcode ты увидишь вывод:

◇ Suite ColdWeather started.
◇ Test someStaticFn() started.
✔ Test someStaticFn() passed after 0.001 seconds.
✔ Suite ColdWeather passed after 0.001 seconds.
✔ Test run with 1 test passed after 0.001 seconds.

Что здесь происходит?

  1. Ты реализовал метод dressWarm() в качестве обычного метода, но использовал макрос сравнения #expect(...)
  2. В методе static someStaticFn() применил атрибут @Test, создал инстанс типа данных ColdWeather() и только затем вызвал метод dressWarm()

Как видно выше, статический метод завершился успехом, поскольку выражение
-10 + -3 == -13 является верным.

А что будет, если временно отключить статический метод и запустить тест для типа данных?

@Suite
struct ColdWeather {
	func dressWarm() {
		#expect(-10 + -3 == -13)
	}

	@Test(.disabled())
	static func someStaticFn() {
		let instance = ColdWeather()
		instance.dressWarm()
	}
}

В таком случае запуск тестов для ColdWeather осуществится без ошибок, но и тестировать попросту будет нечего. Это не является ошибкой, это особенность данного фреймворка:

◇ Suite ColdWeather started.
​✘ Test someStaticFn() skipped.
​✔ Suite ColdWeather passed after 0.001 seconds.
✔ Test run with 1 test passed after 0.001 seconds.

Ограничения для типов Suite

При использовании типов данных вместе с атрибутом @Suite накладываются дополнительные ограничения:

  • Необходимость наличия инициализатора

Если тип содержит методы для тестирования, он должен быть инициализирован без свойств принимающих значения. Инициализатор может быть:

  • явным или неявным;
  • синхронным или асинхронным;
  • бросающим исключения (init() throws);
  • любого уровня доступа (private, internal, public).

Пример:

@Suite
struct FoodTruckTests {
    var batteryLevel = 100

    @Test
    func foodTruckExists() {...} // ✅ OK: есть неявный инициализатор.
}

@Suite
struct CashRegisterTests {
    private init(cashOnHand: Decimal = 0.0) async throws {...}

    @Test
    func calculateSalesTax() {...} // ✅ OK: доступен явный инициализатор.
}

struct MenuTests {
    var foods: [Food]
    var prices: [Food: Decimal]

    @Test
    static func specialOfTheDay() {...} // ✅ OK: функция статическая.
    
    @Test
    func orderAllFoods() {...} // ❌ Ошибка: типу требуется инициализатор.
}

Компилятор выдаст ошибку, если тип данных не удовлетворяет этому требованию.

Атрибут @available можно применять к функции или методу, чтобы ограничить её доступность во время выполнения. Однако типы данных, включая вложенные, не могут быть аннотированы этим атрибутом:

@Suite
struct FoodTruckTests {...} // ✅ OK: тип всегда доступен.

@available(macOS 11.0, *) // ❌ Ошибка: тип данных должен быть всегда доступен.
@Suite
struct CashRegisterTests {...}

@available(macOS 11.0, *)
struct MenuItemTests { // ❌ Ошибка: вложенный тип должен быть доступен.
    @Suite
    struct BurgerTests {...}
}

Компилятор выдаст ошибку, если тип данных нарушает это правило.

Атрибут @available(...) используется для обозначения доступности типа данных или функции.