Какой порядок сообщений и очистки даст этот тест, и почему параллельный subtest не начинает выполнять тело ...

Какой порядок сообщений и очистки даст этот тест, и почему параллельный subtest не начинает выполнять тело сразу?

package config

import (
    "fmt"
    "testing"
)

func TestConfig(t *testing.T) {
    t.Cleanup(func() { fmt.Println("parent cleanup") })
    t.Run("parallel", func(t *testing.T) {
        t.Parallel()
        t.Cleanup(func() { fmt.Println("child cleanup") })
        fmt.Println("child body")
    })
    fmt.Println("parent body")
}
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Сначала будет напечатано parent body, затем child body, после него child cleanup, и последней — parent cleanup. Вызов t.Parallel() приостанавливает subtest до завершения функции родительского теста, поэтому родитель продолжает выполнение после t.Run, а тело subtest запускается позже.

t.Cleanup привязан к конкретному объекту *testing.T: очистка дочернего теста выполняется после его тела, а очистка родительского — после завершения всех его subtests. Зарегистрированные очистители выполняются в обратном порядке регистрации внутри одного теста.

Исторический контекст

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

Механизм t.Cleanup решает другую исходную проблему — гарантированное освобождение ресурсов при успешном, ошибочном или аварийно завершившемся тесте. Область очистки совпадает с областью конкретного *testing.T, что особенно важно для вложенных и параллельных subtests.

Постановка проблемы

Вызов t.Run с параллельным subtest не означает, что subtest немедленно выполняется в отдельной независимой фазе. Если родитель изменяет общую фикстуру после t.Run, а subtest рассчитывает на её состояние, порядок нужно понимать явно.

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

Подробное решение

Когда subtest вызывает t.Parallel(), он сигнализирует тестовому фреймворку, что его выполнение можно продолжить позже. До этого момента subtest приостанавливается, а t.Run позволяет родительской функции продолжить работу. Поэтому parent body печатается раньше child body.

После возврата родительской функции фреймворк разрешает выполнение параллельного subtest. Subtest выполняет своё тело, затем запускает зарегистрированные через t.Cleanup функции. Только после завершения subtest выполняется очистка родителя.

В данном примере порядок такой:

  1. parent body;
  2. child body;
  3. child cleanup;
  4. parent cleanup.

Гарантируется относительный порядок, заданный жизненным циклом тестов, но не следует полагаться на точный порядок вывода между несколькими независимыми параллельными subtests. Для ресурса, используемого только дочерним тестом, очистку следует регистрировать внутри этого subtest:

func TestConfig(t *testing.T) { t.Run("parallel", func(t *testing.T) { resource := openResource() t.Cleanup(func() { resource.Close() }) t.Parallel() use(resource) }) }

На практике подготовку ресурса до t.Parallel() нужно оценивать отдельно. Если подготовка выполняется до вызова, она происходит в последовательной фазе subtest; если ресурс общий, требуется дополнительная синхронизация и договорённость о владении. t.Cleanup не делает доступ к общей переменной безопасным и не заменяет mutex, канал или другой механизм синхронизации.

Ситуация из практики

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

Рассматривались два варианта. Сделать тесты последовательными было бы проще, но увеличило бы время набора и скрыло бы проблемы совместимости с параллельным запуском. Оставить один общий каталог можно было при добавлении блокировок, однако тесты стали бы зависеть от общего состояния и хуже изолировались.

Выбрали отдельный каталог внутри каждого subtest и зарегистрировали его удаление через t.Cleanup этого subtest. В результате очистка выполнялась после тела именно соответствующей проверки, тесты не делили файловое состояние, а параллельный запуск сохранял выигрыш по времени.

Что кандидаты часто упускают

  1. Что произойдёт, если вызвать t.Parallel() после регистрации t.Cleanup?

    Регистрация очистки не запускает её немедленно и не меняет точку приостановки subtest. После t.Parallel() выполнение текущей функции остановится до разрешения со стороны фреймворка, а зарегистрированный cleanup всё равно выполнится после тела subtest. Обычно ресурс можно регистрировать до или после t.Parallel(), но подготовка до него относится к последовательной фазе и может влиять на длительность запуска.

  2. Можно ли использовать t.Cleanup родителя для очистки ресурса, которым пользуются параллельные subtests?

    Можно, если ресурс гарантированно нужен до завершения всех дочерних тестов и его состояние корректно защищено от конкурентного доступа. Очистка родителя выполняется после дочерних subtests, поэтому она не должна завершить ресурс раньше них. Однако один общий ресурс создаёт конкуренцию и усложняет изоляцию; отдельные ресурсы на subtest обычно безопаснее и проще.

  3. Гарантирует ли t.Parallel() безопасную работу с общей переменной?

    Нет. t.Parallel() только управляет расписанием тестов относительно последовательной части и разрешает параллельное выполнение; он не синхронизирует чтения и записи. Если несколько subtests меняют общую переменную, возможны гонка данных и зависимость результата от расписания. Нужно устранить общее изменяемое состояние, использовать локальные копии либо добавить подходящую синхронизацию и проверять тесты с помощью go test -race.