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

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

В предыдущем посте мы видели, как Xcode создает новый класс модульного тестирования для каждой тестовой функции. Вот краткий обзор.

Допустим, у нас есть такой класс модульного тестирования.

class FeatureATests: XCTestCase {
   override func setUpWithError() throws {
      print(">>> FeatureATests setUp \(#function)  \(Unmanaged.passUnretained(self).toOpaque())")
   }
   override func tearDownWithError() throws {
      print(">>> FeatureATests tearDown \(#function)  \(Unmanaged.passUnretained(self).toOpaque())")
   }
   func test_featureA_2() throws {
      print(">>> \(#function)  \(Unmanaged.passUnretained(self).toOpaque())")
   }
   func test_featureA_1() throws {
      print(">>> \(#function)  \(Unmanaged.passUnretained(self).toOpaque())")
   }
}

Мы могли бы ожидать, что будет вызвана функция setUpWithError, за которой следует функция test_featureA_2, затем test_featureA_1 и, наконец, tearDownWithError.

Однако на самом деле происходит то, что новый экземпляр класса FeatureATests создается для каждой тестовой функции, и эти функции вызываются в алфавитном порядке. На самом деле происходит следующее… (обратите внимание, что каждый тест относится к разным экземплярам класса)

>>> FeatureATests setUp setUpWithError()  0x0000600002ab5200
>>> test_featureA_1()  0x0000600002ab520
>>> FeatureATests tearDown tearDownWithError()  0x0000600002ab5200
>>> FeatureATests setUp setUpWithError()  0x0000600002ab6200
>>> test_featureA_2()  0x0000600002ab6200
>>> FeatureATests tearDown tearDownWithError()  0x0000600002ab6200

Итак, что произойдет, если мы рандомизируем тесты?

Чтобы легче увидеть, что происходит, когда мы рандомизируем тесты, давайте создадим несколько копий FeatureATests и назовем их FeatureBTests, FeatureCTests.

Чтобы рандомизировать тесты, выберите схему, а в области тестирования выберите «Параметры…» и отметьте «Случайный порядок выполнения».

Теперь, когда мы запускаем тесты, мы видим, что на самом деле делает рандомизация порядка выполнения.

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

Итак, в следующий раз, когда вы запустите тест, вы можете увидеть такой порядок

test_featureA_2()
test_featureA_1()
test_featureC_2()
test_featureC_1()
test_featureB_1()
test_featureB_2()

но в равной степени вы можете видеть

test_featureC_2()
test_featureC_1()
test_featureA_1()
test_featureA_2()
test_featureB_2()
test_featureB_1()

Главное отметить здесь то, что…

  • Каждая тестовая функция запускается в собственном экземпляре класса.
  • Классы могут выполнять свои функции в любом порядке
  • Все функции класса должны быть выполнены до перехода к следующему классу.
  • Занятия могут проходить в любом порядке

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

Что происходит, когда мы запускаем тесты параллельно?

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

Поскольку тесты выполняются очень быстро, Xcode решает не запускать их параллельно. Итак, вы увидите что-то вроде…

>>> test_featureA_1()
>>> test_featureA_2()
>>> test_featureC_1()
>>> test_featureC_2()
>>> test_featureB_1()
>>> test_featureB_2()

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

class FeatureATests: XCTestCase {
   override func setUpWithError() throws {
      print(">>> FeatureATests setUp \(#function)") 
   }
   override func tearDownWithError() throws {
      print(">>> FeatureATests tearDown \(#function)")
   }
   func test_featureA_2() throws {
      print(">>> \(#function)")
      sleep(1)
   }
   func test_featureA_1() throws {
      print(">>> \(#function)")
      sleep(1)
   }
}

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

Представьте, что у вас есть 120 тестовых классов, каждый из которых выполняется за 5 секунд. Без параллельного выполнения это в общей сложности 10 минут для запуска всех тестов. При включенном параллельном выполнении вы можете увидеть, что тесты выполняются за 1/2 или 1/3 этого времени (в зависимости от того, как Xcode хочет это обрабатывать).

Однако имейте в виду, что тесты выполняются так же быстро, как и самый медленный тестовый класс. Если FeatureATests выполняется 9 минут, а все остальные — 1 минуту, то как минимум тест всегда будет выполняться 9 минут. Если вы заметите, что определенный тестовый класс работает намного медленнее, чем остальные, пытающиеся разбить его на более мелкие фрагменты, вы можете затем вернуться к достижению общих тестов, выполняющихся в два раза быстрее.

Главное, на что следует обратить внимание, это то, что…

  • Каждая тестовая функция запускается в собственном экземпляре класса.
  • Xcode решит, сколько симуляторов будет запускать тесты и какие тестовые классы будут запускаться в каждом симуляторе.
  • Появляющиеся дополнительные симуляторы являются клонами. Обратите внимание, что эти симуляторы не являются одними и теми же симуляторами, это копии, поэтому изменение чего-то вроде UserDefault в одном клоне не повлияет на другой.
  • Тестовые функции в классе будут запускаться либо в алфавитном порядке, либо случайным образом в зависимости от выбранных пользователем настроек.
  • Внутри каждого клона тестовые классы будут выполняться последовательно, а не параллельно, и один тестовый класс должен быть завершен до начала следующего.

Резюме

Даны 3 тестовых класса, FeatureA, FeatureB, FeatureC, каждый из которых владеет 2 тестовыми функциями 1 или 2, если вы….

Запустите в режиме по умолчанию (не случайным или параллельным), вы увидите порядок, подобный…

Запустите как случайный, вы увидите порядок, например…

Запустив параллельно, вы увидите порядок вроде…

Наконец, вы также можете одновременно включить случайный и параллельный тест, что даст что-то вроде этого…

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