Pull to refresh

Вероятностное Unit-тестирование. (Chaos driven Unit Testing.)

Reading time 4 min
Views 3.4K
Все более-менее сложные программные системы содержат ошибки (если и не собственные, то наведённые используемыми библиотеками или по причине неточного осознания поведенческих парадигм используемых фреймворков).
Часто, для тестирования системы на этапе разработки используются Unit-тесты.

Так программист может контролировать поведение системы на контрольных точках и пограничных значениях.
Часто именно неверная отработка пограничных значений приводит к проблемам. И опытные программисты это знают и учитывают при проектировании Unit-тестов.

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

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

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

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

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

Программа компилируется, все Warning'и устранены… но иногда что-то идёт не так… у пользователя (который живёт далеко в домике на островке в тихом океане — приехать к нему и пощупать нет возможности). Программист прокликал и протестировал со своей стороны всё что мог, но ошибки не нашёл. Что же делать?

Любое приложение можно рассматривать как массив взаимосвязанных компонентов C объеденённых в логическую сеть.
Каждый компонент принимает на вход аргументы I, а на выходе даёт результаты O.
Мы составляем генераторы для получения случайных аргументов I, подаём их на вход компонентам C и проверяем выходы O, а также проверяем дополнительными тестами целостность состояния компонента C.

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

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

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

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

До поиска ошибки, этот сценарий можно попытаться сократить до минимальной длинны, так, чтобы ошибка всё ещё проявлялась.
После устранения ошибки этот сценарий должен отрабатывать без сбоев, а мы имеем ещё на 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, …
Чтобы по номеру case прогнать любую проверку в будущем.

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

Для влияния на систему проверки, можно менять выход генераторов аргументов — форсируя больше пересечений… и тем самым сдвигая вероятность наступления тех или иных событий.
Для примера выше, мы можем генерировать слова от 1 до 10 букв и статьи от 1 до 100 символов.
Мы можем поменять условия и генерировать слова в 1-3 буквы а статьи от 1 до 10 символов, соответственно мы можем оказаться в вероятностном поле иных ошибок.
Мы можем также влиять на вероятности выбора доступных операций и либо заставить словарь резко расти, либо резко худеть.
Мы можем даже менять политику вероятностей выбора также случайным образом, подобно ветру, который меняет своё направление…

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

Вероятностное тестирование может ещё на одну ступеньку приблизить нас к имитации конечного пользователя и помочь обнаружить скрытые дефекты.
Tags:
Hubs:
+20
Comments 18
Comments Comments 18

Articles