Применяя тестирование грубой силой, важно всеми силами избегать условий, способных породить ложную уверенность. Для этого необходимо тщательно планировать тесты, уделяя внимание не только тому, какие участки кода подвергать тестированию, но также проектированию стенда (test harness) и выбору тестовой среды. Вы должны постараться протестировать как можно больше путей выполнения кода и взаимодействий между потоками. При этом еще необходимо знать, какие именно варианты протестированы, а какие остались не протестированными .
Хотя метод грубой силы дает некоторую уверенность в правильности кода, гарантировать обнаружение всех ошибок он не может. Однако существует методика, которая гарантированно находит проблемы, если только у вас есть время применить ее к своему коду и подходящее программное обеспечение. Я называю ее комбинаторным имитационным тестированием .
Комбинаторное имитационное тестирование
Название не вполне внятное, поэтому объясню, что я имею в виду. Идея в том, чтобы прогонять код под управлением специальной программы, которая имитирует реальную среду. Вам, наверное, доводилось слышать о программах, которые запускают несколько виртуальных машин на одном физическом компьютере, причём характеристики каждой виртуальной машины и оборудования эмулируются программным супервизором. Здесь всё похоже, только вместо эмулирования системы имитационное ПО записывает последовательности операций доступа к данным, захвата блокировок и атомарных операций в каждом потоке. Затем она применяет правила модели памяти, определенные в С++, чтобы повторить прогон при любой допустимой комбинации операций и таким образом выявить гонки и взаимоблокировки.
Хотя такое исчерпывающее тестирование всех комбинаций гарантированно обнаружит все проблемы, на поиск которых система рассчитана, но для любой программы, кроме самых тривиальных, оно займет чудовищно много времени, потому что количество комбинаций экспоненциально увеличивается с ростом числа потоков и операций, выполняемых в каждом потоке. Лучше применять эту методику к детальным тестам отдельных частей кода, а не ко всему приложению. Очевидный недостаток заключается том, что необходимо специальное имитационное ПО, способное обработать встречающиеся в вашей программе операции.
Таким образом, мы располагаем методикой, подразумевающей многократный прогон тестов при обычных условиях, которая, однако, может пропускать некоторые ошибки, и методикой, предполагающий запуск в специально созданных условиях, которая найдет ошибки с гораздо большей вероятностью. А есть ли еще какие-нибудь варианты?
Третий способ — воспользоваться библиотекой, которая сама обнаруживает проблемы, возникающие при прогоне тестов.
Обнаружение возникающих во время тестирования проблем с помощью специальной библиотеки
Этот вариант не обеспечивает исчерпывающего покрытия, которое дает комбинаторное имитационное тестирование, но все же позволяет выявить многие проблемы за счет специальных реализаций таких библиотечных примитивов синхронизации, как мьютексы, блокировки и условные переменные. Например, часто требуется, чтобы любой доступ к некоторым разделяемым данным, производился, когда захвачен определенный мьютекс. Если бы была возможность проверить, какие мьютексы захвачены в момент доступа к этим данным, то можно было бы сообщить об ошибке в случае, когда нужный мьютекс не захвачен. Пометив разделяемые данные определенным образом, мы смогли бы сообщить библиотеке, что проверять.
Такая реализация библиотеки могла бы также записывать последовательность захватов в случае, когда некоторый поток одновременно удерживает более одного мьютекса. Если другой поток попытается захватить те же мьютексы в другом порядке, то будет зарегистрирована потенциальная взаимоблокировка, даже если при реальном прогоне теста она не возникала.
Для тестирования многопоточного кода могла бы быть полезна и специальная библиотека другого рода, в которой реализации таких примитивов, как мьютексы и условные переменные, позволяют автору теста управлять тем, какой поток захватит блокировку, если ее ожидают несколько потоков, или какой из потоков, ожидающих условную переменную, будет разбужен вызовом notify_one()
. Это дало бы возможность настраивать конкретные сценарии и проверять, что код работает в соответствии с ожиданиями.
Читать дальше