Тестирование многопоточного кода на порядок сложнее, потому что точный порядок выполнения потоков не детерминирован и может изменяться от запуска к запуску. Следовательно, если в коде притаилось какое-то состояние гонки, то даже на одном и том же наборе входных данных программа иногда может работать правильно, а иногда давать ошибку. Наличие потенциальной гонки не означает, что программа будет выдавать ошибку
Ввиду трудностей воспроизведения ошибки, внутренне присущих многопоточным программам, вы должны проектировать тесты очень тщательно. Желательно, чтобы каждый тест проверял как можно меньший участок кода, тогда при возникновении ошибки ее будет проще изолировать. Конкретно, проверять правильность работы операций помещения и извлечения элементов в параллельной очереди лучше напрямую, а не путем тестирования всего куска кода, в котором эта очередь используется. Очень помогает еще на этапе проектирования кода думать о том, как он будет тестироваться. См. по этому поводу раздел о тестопригодности ниже в этой главе.
Имеет также смысл устранять из тестов параллелизм, так как это позволяет убедиться, что проблема не связана с параллельным доступом. Если проблема проявляется даже при однопоточной работе, то это самая обычная ошибка, не имеющая отношения к параллелизму. Это особенно важно, когда вы пытаетесь установить причины ошибки, произошедшей «в поле», а не в тестовом окружении. Если ошибка возникает в многопоточной части программы, то это еще не значит, что она как-то связана с параллелизмом. При использовании пулов потоков обычно имеется конфигурационный параметр, определяющий число рабочих потоков. Если вы управляете потоками вручную, то нужно будет модифицировать код, так чтобы в тесте работал только один поток. Как бы то ни было, если удастся воспроизвести ошибку в однопоточном варианте программы, то параллелизм можно исключить из числа возможных причин. С другой стороны, если проблема исчезает при работе в
При тестировании параллельного кода важна не только структура самого кода, но и структура теста и тестовой среды. Все в том же примере параллельной очереди необходимо проверить следующие случаи.
• Один поток вызывает push()
или pop()
для проверки работоспособности очереди на самом простом уровне.
• Один поток вызывает push()
для пустой очереди, а второй в это время вызывает pop()
.
• Несколько потоков вызывают push()
для пустой очереди.
• Несколько потоков вызывают push()
для заполненной очереди.
• Несколько потоков вызывают pop()
для пустой очереди.
• Несколько потоков вызывают pop()
для заполненной очереди.
• Несколько потоков вызывают pop()
для частично заполненной очереди, в которой недостаточно элементов для удовлетворения всех потоков.
• Несколько потоков вызывают push()
, а один вызывает pop()
для пустой очереди.
• Несколько потоков вызывают push()
, а один вызывает pop()
для заполненной очереди.
• Несколько потоков вызывают push()
и несколько потоков вызывают pop()
для пустой очереди.
• Несколько потоков вызывают push()
и несколько потоков вызывают pop()
для заполненной очереди.
Проверив все эти и другие случаи, вы затем должны учесть дополнительные параметры тестовой среды.
• Что понимается под «несколькими потоками» в каждом случае (3, 4, 1024)?
• Достаточно ли в системе процессорных ядер, чтобы каждый поток работал на отдельном ядре?
• Какова архитектура процессора, на котором будет прогоняться тест?
• Как обеспечить подходящее планирование для циклов «while» в тестах?
В зависимости от ситуации может быть необходимо принять во внимание и другие факторы. Из четырех приведённых выше аспектов тестовой среды первый и последний относятся к структуре самого теста (и рассматриваются в разделе 10.2.5), а оставшиеся два — к физической тестовой системе. Сколько потоков использовать, определяется конкретной программой, но способов структурировать тесты для получения нужного планирования потоков существует несколько. Прежде чем рассматривать их, поговорим о том, как следует проектировать код, чтобы его было легко тестировать.
10.2.3. Проектирование с учетом тестопригодности
Тестировать многопоточный код трудно, поэтому вы должны сделать все возможное, чтобы облегчить эту задачу. И едва ли не самое важное — проектировать код с учетом тестопригодности. Для однопоточных программ на эту тему написано немало, и многие рекомендации применимы и к многопоточному случаю. Вообще говоря, код проще тестировать, если он написан с соблюдением следующих принципов.
• Обязанности всех функций и классов четко очерчены.
• Каждая функция короткая и решает ровно одну задачу.