10.2.3. Проектирование с учетом тестопригодности
Тестировать многопоточный код трудно, поэтому вы должны сделать все возможное, чтобы облегчить эту задачу. И едва ли не самое важное — проектировать код с учетом тестопригодности. Для однопоточных программ на эту тему написано немало, и многие рекомендации применимы и к многопоточному случаю. Вообще говоря, код проще тестировать, если он написан с соблюдением следующих принципов.
• Обязанности всех функций и классов четко очерчены.
• Каждая функция короткая и решает ровно одну задачу.
• Тесты способны полностью контролировать окружение тестируемого кода.
• Код, выполняющий конкретную тестируемую операцию, находится приблизительно в одном месте, а не разбросан но всей системе.
• Автор сначала думал о том, как будет тестировать код, а только потом приступал к его написанию.
Все это остается в силе и для многопоточного кода. Я бы даже сказал, что думать о тестопригодности многопоточной программы даже важнее, чем однопоточной, поскольку тестировать ее гораздо труднее. Очень важен последний пункт: даже если вы не считаете нужным писать тесты раньше кода, все равно стоит заранее подумать о том, как вы будете тестировать — какие входные данные использовать, при каких условиях могут проявиться проблемы, какие способы обращения к коду могут выявить потенциальные проблемы и т.д.
Один из лучших способов проектирования параллельного кода, пригодного для тестирования, — устранить параллелизм. Если программу удается разбить на части, которые отвечают за взаимодействие потоков, и части, которые оперируют данными внутри одного потока, то проблема сильно упрощается. Части, оперирующие данными, к которым может обращаться только один поток, можно тестировать, применяя хорошо известные методы. А трудный для тестирования параллельный код, который отвечает за взаимодействие потоков и должен гарантировать, что в каждый момент времени только один поток обращается к конкретному блоку данных, теперь оказывается гораздо короче и обозримее.
Например, приложение, спроектированное в виде многопоточного конечного автомата, можно разбить на несколько частей. Логику управления состояниями в каждом потоке, отвечающую за правильность переходов и операций для любого возможного набора входных событий, можно тестировать независимо, применяя стандартные методы для однопоточного случая. При этом входные событие, которые реально должны были бы поступать от других потоков, будет поставлять тестовая среда. После этого независимо можно протестировать основной код конечного автомата и код маршрутизации сообщений и убедиться, что события доставляются нужному потоку в правильном порядке; при этом специально для тестов будет написана простая логика работы в каждом состоянии.
Или, если получится разбить программу на несколько блоков вида читать разделяемые данные / преобразовать данные / обновить разделяемые данные, то части преобразовать данные можно будет протестировать с помощью стандартных методов, поскольку это обычный однопоточный код. И трудная задача тестирования многопоточных преобразований будет сведена к тестированию чтения и обновления разделяемых данных, что гораздо проще.
Нужно обращать особое внимание на библиотечные вызовы, в которых могут использоваться внутренние переменные для хранения состояния, поскольку эти переменные становятся разделяемыми, если к библиотечным функциям обращаются сразу несколько потоков. Проблема в том, что сразу не очевидно, что код обращается к разделяемым данным. Впрочем, со временем вы запоминаете такие функции, потому что они, словно болячка, постоянно напоминают о себе. Тогда можно либо добавить подходящую защиту и синхронизацию, либо пользоваться другими функциями, безопасными для доступа из нескольких потоков.
Проектирование многопоточного кода с учетом тестопригодности не сводится к структурированию программы с целью уменьшить объем кода, имеющего дело с параллелизмом, и внимательному обращению с библиотечными функциями. Полезно также помнить о вопросах, которые вы задаете себе при анализе кода (см. раздел 10.2.1). Они, правда, не имеют прямого отношения к тестированию и тестопригодности, но, постоянно держа в уме вопросы тестирования, вы будете принимать более правильные проектные решения, которые затем это самое тестирование облегчат.
Итак, мы поговорили о том, как проектировать код с учетом тестопригодности и но возможности отделять «параллельные» части (например, потокобезопасные контейнеры и логику событий конечного автомата) от «однопоточных» (которые все же могут взаимодействовать с другими потоками при посредстве параллельных частей). А теперь рассмотрим некоторые приёмы тестирования параллельного кода.