Вероятностное Модульное Тестирование. (Модульное Тестирование, Управляемое Хаосом.)

Все более или менее сложные программные системы содержат ошибки (если не свои, то вызванные используемыми библиотеками или из-за неточного осознания поведенческих парадигм используемых фреймворков).

Модульные тесты часто используются для тестирования системы на этапе разработки.

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

Зачастую именно неправильная обработка граничных значений приводит к проблемам.

И опытные программисты это знают и учитывают при разработке Unit-тестов.

Удобство Unit-тестов еще и в том, что при изменении кода вы рассчитываете получить предсказуемые результаты и проводите полностью автоматическое тестирование по существующим сценариям, чтобы быстро выявить проблемы, вызванные изменениями.

Например, вы пишете код для работы на Intel и PPC, разрабатываете его на Intel, но учитываете порядок байтов.

Потом запускаешь свои Unit-тесты, сравниваешь выходные данные с эталоном и находишь несоответствия — видно, что забыл где-то байты перевернуть — поправляешь — все в порядке.

Однако любой пользователь всегда несет в себе элемент случайности.

Опытный программист сочетает в себе талант качественного тестировщика и может отловить множество ошибок еще до выхода программы.

Если программа делает больше, чем просто печатает «Hello World!», то скрытые ошибки в любом случае остаются.

Это могут быть и логические ошибки.

Программа компилируется, все Предупреждения удаляются.

но иногда что-то идёт не так.

для пользователя (который живёт далеко в доме на острове в Тихом океане - нет возможности подойти к нему и потрогать) .

Программист позвонил и протестировал со своей стороны всё, что мог, но ошибок не обнаружил.

Что делать? Любое приложение можно рассматривать как массив взаимосвязанных компонентов C, объединенных в логическую сеть.

Каждый компонент принимает аргументы I в качестве входных данных и выдает результаты O в качестве выходных данных.

Составляем генераторы для получения случайных аргументов I, подаем их на вход компонентам C и проверяем выходы O, а также проверяем целостность состояния компонента C дополнительными тестами.

Поэтому мы тестируем каждый компонент со случайным набором данных.

Этот метод можно распространить на всю сеть, состоящую из компонентов, или на выбранные подсети.

При таком стресс-тестировании случайные данные обязательно найдут определенные скрытые граничные точки и проверят те ветви ваших условий, которых ваши вымышленные тесты не достигли бы при обычных обстоятельствах.

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

Конечно, на 1 этапе вам придется настроить подсистему контроля целостности, но дальше все пойдет как по маслу.

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

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

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

Можно выделить следующие операции:

  • Добавить слово
  • Придать существующему слову значение
  • Удалить слово
  • Проверьте целостность
Структура тестового кода (фрагмент Objective-C):
 
 srand(time(0));
 
 NSMutableString * log = [NSMutableString string];// for commands
 int prev = -1;
 unsigned i;
 #define ST_COUNT 2000
 
 id model = [SomeModelFactory createModelObject];
 
 for (i = 0; !status && i < ST_COUNT; i++)
 {
     int todo;
     do
     {
         todo = rand() % 4;
     }
      while (3 == todo && todo == prev);
     prev = todo;
 
     if (i + 1 == ST_COUNT)// last iter.
         todo = 3;// force int. check
 
     switch (todo)
      {
         case 0:// add new word to the model
         {
             …
         }
         case 1:// set existing word
         {
             …
         }
         case 2:// remove word
         {
             …
         }
         case 3:// pint. check
         {
             if (i + 1 == ST_COUNT || rand() % 2)
             {
                 …
                 status = 3;// set some error code if fail
             }
         }
     }
 }
 
 if (status)
 {
     [log writeToFile:@"/tmp/commands.log"  atomically:YES   encoding:NSUTF8StringEncoding   error:NULL ];
     exit(status);
 }
 
Генератор аргументов:
 
 char genChar()
 {
     // allowed chars
     static char allowed[] = "ABCDEFGHIJKLMNOPQRSTUVWXUZabcdefghijklmnopqrstuvwxyz1234567890/";
     return allowed[rand() % (sizeof(allowed)-1)];
 }
 
 NSString* genWord(int min, int max)
 {
     NSMutableString * res = [NSMutableString string];
 
     if (max < min)
         max = min;
 
     int toGen = min + rand() % (max - min + 1); 
 
     int i;
     for (i = 0; i < toGen; i++)
          [res appendFormat:@"%c",genChar()];
 
     return res;
 }
 
Все логи с ошибками можно переместить из /tmp, например, в папку Issues, в папки Case-1, Case-2,.

Использовать номер дела для проведения любой проверки в будущем.

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

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

При проведении операций мы самостоятельно сохраняем состояние системы в обычном словаре NSMutableDictionary, а при проверке целостности проверяем, что количество слов в контрольном и созданном словарях одинаково, что все слова найдены и имеют одинаковый текст статьи как в тестовом примере.

Что слово можно получить по индексу, и то же самое слово можно получить по индексу.

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

и тем самым сместив вероятность возникновения определенных событий.

В приведенном выше примере мы можем генерировать слова от 1 до 10 букв и статьи от 1 до 100 символов.

Мы можем изменить условия и генерировать слова из 1-3 букв и артикли из 1-10 символов, поэтому мы можем попасть в вероятностное поле других ошибок.

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

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

Фактически только благодаря методу вероятностного тестирования в нашем проекте мы поймали 5 скрытых и очень сложных ошибок в уже протестированном двигателе, в котором не было видимых намеков на неисправность! Вероятностное тестирование может на шаг приблизить нас к моделированию конечного пользователя и помочь обнаружить скрытые дефекты.

Теги: #юнит-тестирование #Cocoa #objective-c #тестирование ИТ-систем

Вместе с данным постом часто просматривают: