Верификация программного обеспечения

Тестирование программного кода (покрытия)

Разбить на страницы
Показывать лекцию целиком

6.1. Тест-планы

Тестовые примеры, рассматриваемые в предыдущих разделах, не существуют сами по себе - каждый тестовый пример проверяет одну ситуацию в работе системы, но вся совокупность тестовых примеров должна полностью проверять всю функциональность системы. В связи с этим описания тестовых примеров объединяют в документы, называемыми тест-планами.

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

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

Существует несколько причин для объединения описаний тестовых примеров в единый документ или несколько документов.

  • Единая схема идентификации и трассировки тестовых примеров

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

  • Объединение тестовых примеров в смысловые группы

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

  • Внесение изменений в тестовые примеры

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

  • Определение последовательности тестирования

    Одно из важных свойств тестового примера - его независимость. Это означает, что результат выполнения тестового примера не должен изменяться в зависимости от того, какие тесты выполнялись до него. Как правило, независимость тестовых примеров достигается полной реинициализацией тестового окружения перед выполнением каждого нового тестового примера. Однако, часто возникают ситуации, в которых, для экономии времени выполнения, тесты объединяются в последовательности, где каждый следующий тестовый пример использует состояние тестового окружения или тестируемой системы, достигнутое во время предыдущего теста. Такие связанные тестовые примеры должны быть отдельно помечены для того, чтобы сохранить корректный порядок их следования.

  • 6.1.1. Типовая структура тест-плана

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

    Каждый тестовый пример в этом тест-плане имеет уникальный номер и ссылку на тест-требование, на основе которого он написан.

    Общее описание теста помогает при сопровождении тест-планов - внесении изменений при изменении системы, инспекциях тест-планов, выявляющих несогласованность и т.п.

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

    Тест-план

    Тестовый пример 1
    Номер тест-требования: 2а, 2b
    Описание теста: В данном тесте проверяется правильность 
    вычисления значения контрольной суммы (поля CRC) 
    при непустом значении поля CRC и нулевых значениях элементов записи.
    Входные данные: CRC = 12345, A=0, B=0, C=0, D=0
    Ожидаемые выходные данные: CRC = 0, A=0, B=0, C=0, D=0, Empty = TRUE
    Сценарий теста: 
       1. Установка значения поля CRC в 12345
       2. Установка значений полей A-F в 0
       3. Вызов функции Set_CRC
       4. Проверка значений CRC на 0 и Empty на TRUE
    Тестовый пример 2
    Номер тест-требования: 2a
    Описание теста: В данном тесте проверяется соответствие 
    алгоритма вычисления поля CRC, заданному в спецификации требований.
    Входные данные: CRC = 0, A-D заполнены байтами 01010101b
    Ожидаемые выходные данные: CRC = 0111100b, Empty = FALSE
    Сценарий теста: 
       1. Установка значения поля CRC в 0
       2. Заполнение байт полей A-D байтами 01010101b
       3. Вызов функции Set_CRC
       4. Проверка значений CRC на 0111100b и Empty на FALSE
    Тестовый пример 3
    Номер тест-требования: 2a
    Описание теста: В данном тесте проверяется неизменность 
    полей A-F записи при вычислении поля CRC (подсчете контрольной суммы).
    Входные данные: CRC = 0, A-D заполнены байтами 01010101b
    Ожидаемые выходные данные: A-D заполнены байтами 01010101b, 
    Сценарий теста: 
       1. Установка значения поля CRC в 0
       2. Заполнение байт полей A-D байтами 01010101b
       3. Вызов функции Set_CRC
       4. Проверка значений байт полей A-D на 01010101b

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

    6.2. Оценка качества тестируемого кода - статистика выполнения тестов

    В результате выполнения каждого тестового примера тестовое окружение сравнивает ожидаемые и реальные выходные значения. В случае, если эти значения совпадают, тест считается пройденным, т.к. система выдала именно те выходные значения, которые ожидались; в противном случае тест считается не пройденным.

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

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

  • Вывод количества пройденных и количества не пройденных тестовых примеров, а также их общего количества.

    Например,

    180 test cases passed
    20 test cases failed
    200 test cases total
  • 1 + вывод идентификаторов не прошедших тестовых примеров. Позволяет локализовать тестовые примеры, потенциально выявившие дефект.

    Например,

    Invoking test case 1 … Passed
    Invoking test case 2 … Failed
    Invoking test case 3 … Failed
    <…>
    Invoking test case 200 … Passed
    
    Final stats:
    180 test cases passed
    20 test cases failed
    200 test cases total
  • 2 + вывод не совпавших ожидаемых и реальных выходных данных. Позволяет проводить более глубокий анализ причин неуспешного прохождения тестового примера.

    Например,

    Invoking test case 1 … Passed
    ---
    Invoking test case 2 … Failed
    Expected values:      Actual values:
      A = 200        A = 0
      B = 450        B = 0
      Message = "Submenu 1"    Message = ""
    ---
    Invoking test case 3 … Failed
    Expected values:      Actual values:
      A = 0          A = 200
      B = 0          B = 300
      Message = ""      Message = "Main Menu"
    ---
    <…>
    Invoking test case 200 … Passed
    ---
    
    Final Stats
    180 test cases passed
    20 test cases failed
    200 test cases total
  • 2 + вывод всех ожидаемых и реальных выходных данных. Вариант предыдущего пункта.

    Например,

    Invoking test case 1 … Passed
    ---
    Invoking test case 2 … Failed
    Expected values:      Actual values:
      A = 200        A = 0        FAIL
      B = 450        B = 0        FAIL
      C = 500        C = 500        P
      D = 600        D = 600        P
      Message = "Submenu 1"    Message = ""      FAIL
    ---
    Invoking test case 3 … Failed
    Expected values:      Actual values:
      A = 0        A = 200        FAIL
      B = 0        B = 300        FAIL
      C = 500        C = 500        P
      D = 600        D = 600        P
      Message = ""      Message = "Main Menu"    FAIL
    ---
    <…>
    Invoking test case 200 … Passed
    ---
    
    Final Stats
    180 test cases passed
    20 test cases failed
    200 test cases total
  • Полный вывод ожидаемых и реальных выходных данных с отметками о совпадении и несовпадении и отметками об успешном/неуспешном завершении для каждого тестового примера.

    Например,

    Invoking test case 1 … Passed
      A = 0        A = 0      P
      B = 0        B = 0      P
      C = 500        C = 500      P
      D = 600        D = 600      P
      Message = ""      Message = ""    P
    ---
    Invoking test case 2 … Failed
    Expected values:      Actual values:
      A = 200        A = 0      FAIL
      B = 450        B = 0      FAIL
      C = 500        C = 500      P
      D = 600        D = 600      P
      Message = "Submenu 1"    Message = ""    FAIL
    ---
    Invoking test case 3 … Failed
    Expected values:      Actual values:
      A = 0        A = 200      FAIL
      B = 0        B = 300      FAIL
      C = 500        C = 500      P
      D = 600        D = 600      P
      Message = ""      Message = "Main Menu"  FAIL
    ---
    <…>
    Invoking test case 200 … Passed
      Message = "Submenu 1"    Message = "Submenu 1  P
      Prompt  = ">"      Prompt = ">"    P
    ---
    Final Stats
    180 test cases passed
    20 test cases failed
    200 test cases total
  • Более детально различные форматы отчетов о тестировании будут рассмотрены позднее, а пока остановимся более подробно на важном критерии оценки качества системы тестов и степени полноты тестирования системы - уровне покрытия программного кода тестами.

    6.3. Покрытие программного кода

    6.3.1. Понятие покрытия

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

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

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

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

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

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

    6.3.2. Уровни покрытия

    6.3.3. По строкам программного кода (Statement Coverage)

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

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

    Например, для полного покрытия всех строк следующего участка программного кода на языке C достаточно одного тестового примера:

    Вход: condition = true; Ожидаемый выход: *p = 123.
    int* p = NULL;
    if (condition)
        p = variable;
    *p = 123;

    Даже если в состав тестов не будет входить тестовый пример, проверяющий работу фрагмента при значении condition = false, код будет покрыт. Однако, в случае condition = false выполнение фрагмента вызовет ошибку.

    Аналогичные проблемы возникают при проверке циклов do … while - при данном уровне покрытия достаточно выполнение цикла только один раз, при этом метод совершенно нечувствителен к логическим операторам || и

    Другой особенностью данного метода является зависимость уровня покрытия от структуры программного кода. На практике часто не требуется 100% покрытия программного кода, вместо этого устанавливается допустимый уровень покрытия, например 75%. Проблемы могут возникнуть при покрытии следующего фрагмента программного кода:

    if (condition)
       functionA();
    else
       functionB();

    Если functionA() содержит 99 операторов, а functionB() - один оператор, то единственного тестового примера, устанавливающего condition в true, будет достаточно для достижения необходимого уровня покрытия. При этом аналогичный тестовый пример, устанавливающий значение condition в false, даст слишком низкий уровень покрытия.

    6.3.3.1. По веткам условных операторов (Decision Coverage)

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

    Также данный метод называют: branch coverage, all-edges coverage, basis path coverage, DC, C2, decision-decision-path.

    В отличие от предыдущего уровня покрытия данный метод учитывает покрытие условных операторов с пустыми ветками. Так, для покрытия по веткам участка программного кода

    a = 0;
    if (condition) {
      a = 1;
    }

    необходимы два тестовых примера:

    1. Вход: condition = true; Ожидаемый выход: a = 1;
    2. Вход: condition = false; Ожидаемый выход: a = 0;

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

    if ( condition1  ( condition2 || function1() ) )
        statement1;
    else
        statement2;

    полное покрытие по веткам может быть достигнуто при помощи двух тестовых примеров:

    1. Вход: condition1 = true, condition2 = true
    2. Вход: condition1 = false, condition2 = true/false (любое значение)

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

    3. Вход: condition1 = true, condition2 = false.

    6.3.3.2. По компонентам логических условий

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

    6.3.3.3. Покрытие по условиям (Condition Coverage)

    Для обеспечения полного покрытия по данному методу каждая компонента логического условия в результате выполнения тестовых примеров должна принимать все возможные значения, но при этом не требуется, чтобы само логическое условие принимало все возможные значения. Так, например, при тестировании следующего фрагмента:

    if (condition1 | condition2)
      functionA();
    else
      functionB();

    для покрытия по условиям потребуется два тестовых примера:

    (1)  Вход: condition1 = true, condition2 = false
    (2)  Вход: condition1 = false, condition2 = true.

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

    6.3.3.4. Покрытие по веткам/условиям (Condition/Decision Coverage)

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

    Для покрытия рассмотренного выше фрагмента с условием condition1 | condition2 потребуется 2 тестовых примера:

    1.  Вход: condition1 = true, condition2 = true
    2.  Вход: condition1 = false, condition2 = false.

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

    6.3.3.5. Покрытие по всем условиям (Multiple Condition Coverage)

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

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

    Еще одним недостатком метода является зависимость количества тестовых примеров от структуры логического выражения. Так, для условий, содержащих одинаковое количество компонент и логических операций:

    a  b  (c || (d  e))
    ((a || b)  (c || d))  e

    потребуется разное количество тестовых примеров. Для первого случая для полного покрытия нужно 6 тестов, для второго - 11.

    6.3.4. Метод MC/DC для уменьшения количества тестовых примеров при 3-м уровне покрытия кода

    Для уменьшения количества тестовых примеров при тестировании логических условий фирмой Boeing был разработан модифицированный метод покрытия по веткам/условиям (Modified Condition/Decision Coverage или MC/DC) [25, 26]. Данный метод широко используется при верификации бортового авиационного программного обеспечения согласно процессам стандарта DO-178B [7].

    Для обеспечения полного покрытия по этому методу необходимо выполнение следующих условий:

  • каждое логическое условие должно принимать все возможные значения;
  • каждая компонента логического условия должна хотя бы один раз принимать все возможные значения;
  • должно быть показано независимое влияние каждой из компонент на значение логического условия, т.е. влияние при фиксированных значениях остальных компонент.
  • Покрытие по этой метрике требует достаточно большого количества тестов для того, чтобы проверить каждое условие, которое может повлиять на результат выражения, однако это количество значительно меньше, чем требуемое для метода покрытия по всем условиям. В таблице 6.1 приведены примеры тестовых наборов, необходимых для тестирования логических блоков по MC/DC. Так, например, для блока OR достаточно n+1 тестовых примеров, где n - количество входов логического блока. Первый тестовый пример показывает, что при нулевых значениях входов значение выхода также нулевое. В каждом из следующих n примеров значение каждого входа устанавливается в 1, чем показывается независимое влияние входов на значение выхода.

    Логические блоки и определенные для них тестовые наборы
    AND блок. Реализует логическую функцию NAND блок. Реализует логическую функцию
    № набора 1 2 3 4 ··· n + 1 № набора 1 2 3 4 ··· n + 1
    Вход 1T F T T ··· T Вход 1T F T T ··· T
    Вход 2T T F T ··· T Вход 2T T F T ··· T
    Вход 3T T T F ··· T Вход 3T T T F ··· T
    ······ ··· ··· ··· ··· ··· ······ ··· ··· ··· ··· ···
    Вход nT T T T ··· F Вход nT T T T ··· F
    ВыходT F F F ··· F ВыходF T T T ··· T
    OR блок. Реализует логическую функцию NOR блок. Реализует логическую функцию
    № набора 1 2 3 4 ··· n + 1 № набора 1 2 3 4 ··· n + 1
    Вход 1F T F F ··· F Вход 1F T F F ··· F
    Вход 2F F T F ··· F Вход 2F F T F ··· F
    Вход 3F F F T ··· F Вход 3F F F T ··· F
    ······ ··· ··· ··· ··· ··· ······ ··· ··· ··· ··· ···
    Вход nF F F F ··· T Вход nF F F F ··· T
    ВыходF T T T ··· T ВыходT F F F ··· F

    6.3.5. Анализ покрытия

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

  • анализ должен подтвердить, что полнота покрытия тестами структуры кода соответствует требуемому виду покрытия и заданному минимально допустимому проценту покрытия;
  • анализ полноты покрытия тестами структуры кода может быть выполнен с использованием исходного текста, если программное обеспечение не относится к уровню A. Для уровня А необходимо проверить объектный код, сгенерированный компилятором, и выяснить, трассируется ли он в исходный текст или нет. Если объектный код не трассируется в исходный текст, должны быть проведены поверки объектного кода на предмет правильности генерации последовательности команд. Примером объектного кода, который напрямую не трассируется в исходный текст, но генерируется компилятором, может быть проверка выхода за заданные границы массива;
  • анализ должен подтвердить правильность передачи данных и управления между компонентами кода.
  • Анализ полноты покрытия тестами может выявить часть исходного кода, которая не исполнялась в ходе тестирования. Для разрешения этого обстоятельства могут потребоваться дополнительные действия в процессе проверки программного обеспечения. Эта неисполняемая часть кода может быть результатом:

  • недостатков в формировании тестовых примеров или тестовых процедур, основанных на требованиях. В этом случае должен быть дополнен набор тестовых примеров или изменены тестовые процедуры для обеспечения покрытия упущенной части кода. При этом может потребоваться пересмотр метода (методов), используемого для проведения анализа полноты тестов на основе требований;
  • неадекватности в требованиях на программное обеспечение: В этом случае должны быть модифицированы требования на программное обеспечение, разработаны и выполнены дополнительные тестовые примеры и тестовые процедуры;
  • "мертвого" кода. Этот код должен быть удален, и необходимо провести анализ для оценки эффекта удаления и необходимости перепроверки;
  • дезактивируемого кода. Для дезактивируемого кода, который не предполагается к выполнению в каждой конфигурации, сочетание анализа и тестов должно продемонстрировать возможности средств, которыми непреднамеренное исполнение такого кода предотвращается, изолируется или устраняется. Для дезактивируемого кода, который выполняется только при определенных конфигурациях, должна быть установлена нормальная эксплуатационная конфигурация для исполнения этого кода, и для нее должны быть разработаны дополнительные тестовые примеры и тестовые процедуры, удовлетворяющие целям полноты покрытия тестами структуры кода;
  • избыточности условия. Логика работы такого условия должна быть пересмотрена. Например, в условии if (A B || !B) принципиально невозможно проверить, что часть условия A B будет равна False в случае, когда A=True и B=False, так как вторая часть условия (!B) будет равна True, и общий результат логического выражения будет True ;
  • защитного кода. Эта часть кода используется для предотвращения исключительных ситуаций, которые могут возникнуть в процессе работы программы. Как пример, это может быть ветка default в операторе выбора switch, причем входное условие оператора switch может принимать определенные значения, которые он описывает, и, как следствие, ветка default никогда не будет выполнена.
  • Страницы:

    6.1. Тест-планы

    Тестовые примеры, рассматриваемые в предыдущих разделах, не существуют сами по себе - каждый тестовый пример проверяет одну ситуацию в работе системы, но вся совокупность тестовых примеров должна полностью проверять всю функциональность системы. В связи с этим описания тестовых примеров объединяют в документы, называемыми тест-планами.

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

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

    Существует несколько причин для объединения описаний тестовых примеров в единый документ или несколько документов.

  • Единая схема идентификации и трассировки тестовых примеров

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

  • Объединение тестовых примеров в смысловые группы

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

  • Внесение изменений в тестовые примеры

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

  • Определение последовательности тестирования

    Одно из важных свойств тестового примера - его независимость. Это означает, что результат выполнения тестового примера не должен изменяться в зависимости от того, какие тесты выполнялись до него. Как правило, независимость тестовых примеров достигается полной реинициализацией тестового окружения перед выполнением каждого нового тестового примера. Однако, часто возникают ситуации, в которых, для экономии времени выполнения, тесты объединяются в последовательности, где каждый следующий тестовый пример использует состояние тестового окружения или тестируемой системы, достигнутое во время предыдущего теста. Такие связанные тестовые примеры должны быть отдельно помечены для того, чтобы сохранить корректный порядок их следования.

  • 6.1.1. Типовая структура тест-плана

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

    Каждый тестовый пример в этом тест-плане имеет уникальный номер и ссылку на тест-требование, на основе которого он написан.

    Общее описание теста помогает при сопровождении тест-планов - внесении изменений при изменении системы, инспекциях тест-планов, выявляющих несогласованность и т.п.

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

    Тест-план

    Тестовый пример 1
    Номер тест-требования: 2а, 2b
    Описание теста: В данном тесте проверяется правильность 
    вычисления значения контрольной суммы (поля CRC) 
    при непустом значении поля CRC и нулевых значениях элементов записи.
    Входные данные: CRC = 12345, A=0, B=0, C=0, D=0
    Ожидаемые выходные данные: CRC = 0, A=0, B=0, C=0, D=0, Empty = TRUE
    Сценарий теста: 
       1. Установка значения поля CRC в 12345
       2. Установка значений полей A-F в 0
       3. Вызов функции Set_CRC
       4. Проверка значений CRC на 0 и Empty на TRUE
    Тестовый пример 2
    Номер тест-требования: 2a
    Описание теста: В данном тесте проверяется соответствие 
    алгоритма вычисления поля CRC, заданному в спецификации требований.
    Входные данные: CRC = 0, A-D заполнены байтами 01010101b
    Ожидаемые выходные данные: CRC = 0111100b, Empty = FALSE
    Сценарий теста: 
       1. Установка значения поля CRC в 0
       2. Заполнение байт полей A-D байтами 01010101b
       3. Вызов функции Set_CRC
       4. Проверка значений CRC на 0111100b и Empty на FALSE
    Тестовый пример 3
    Номер тест-требования: 2a
    Описание теста: В данном тесте проверяется неизменность 
    полей A-F записи при вычислении поля CRC (подсчете контрольной суммы).
    Входные данные: CRC = 0, A-D заполнены байтами 01010101b
    Ожидаемые выходные данные: A-D заполнены байтами 01010101b, 
    Сценарий теста: 
       1. Установка значения поля CRC в 0
       2. Заполнение байт полей A-D байтами 01010101b
       3. Вызов функции Set_CRC
       4. Проверка значений байт полей A-D на 01010101b

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

    6.2. Оценка качества тестируемого кода - статистика выполнения тестов

    В результате выполнения каждого тестового примера тестовое окружение сравнивает ожидаемые и реальные выходные значения. В случае, если эти значения совпадают, тест считается пройденным, т.к. система выдала именно те выходные значения, которые ожидались; в противном случае тест считается не пройденным.

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

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

  • Вывод количества пройденных и количества не пройденных тестовых примеров, а также их общего количества.

    Например,

    180 test cases passed
    20 test cases failed
    200 test cases total
  • 1 + вывод идентификаторов не прошедших тестовых примеров. Позволяет локализовать тестовые примеры, потенциально выявившие дефект.

    Например,

    Invoking test case 1 … Passed
    Invoking test case 2 … Failed
    Invoking test case 3 … Failed
    <…>
    Invoking test case 200 … Passed
    
    Final stats:
    180 test cases passed
    20 test cases failed
    200 test cases total
  • 2 + вывод не совпавших ожидаемых и реальных выходных данных. Позволяет проводить более глубокий анализ причин неуспешного прохождения тестового примера.

    Например,

    Invoking test case 1 … Passed
    ---
    Invoking test case 2 … Failed
    Expected values:      Actual values:
      A = 200        A = 0
      B = 450        B = 0
      Message = "Submenu 1"    Message = ""
    ---
    Invoking test case 3 … Failed
    Expected values:      Actual values:
      A = 0          A = 200
      B = 0          B = 300
      Message = ""      Message = "Main Menu"
    ---
    <…>
    Invoking test case 200 … Passed
    ---
    
    Final Stats
    180 test cases passed
    20 test cases failed
    200 test cases total
  • 2 + вывод всех ожидаемых и реальных выходных данных. Вариант предыдущего пункта.

    Например,

    Invoking test case 1 … Passed
    ---
    Invoking test case 2 … Failed
    Expected values:      Actual values:
      A = 200        A = 0        FAIL
      B = 450        B = 0        FAIL
      C = 500        C = 500        P
      D = 600        D = 600        P
      Message = "Submenu 1"    Message = ""      FAIL
    ---
    Invoking test case 3 … Failed
    Expected values:      Actual values:
      A = 0        A = 200        FAIL
      B = 0        B = 300        FAIL
      C = 500        C = 500        P
      D = 600        D = 600        P
      Message = ""      Message = "Main Menu"    FAIL
    ---
    <…>
    Invoking test case 200 … Passed
    ---
    
    Final Stats
    180 test cases passed
    20 test cases failed
    200 test cases total
  • Полный вывод ожидаемых и реальных выходных данных с отметками о совпадении и несовпадении и отметками об успешном/неуспешном завершении для каждого тестового примера.

    Например,

    Invoking test case 1 … Passed
      A = 0        A = 0      P
      B = 0        B = 0      P
      C = 500        C = 500      P
      D = 600        D = 600      P
      Message = ""      Message = ""    P
    ---
    Invoking test case 2 … Failed
    Expected values:      Actual values:
      A = 200        A = 0      FAIL
      B = 450        B = 0      FAIL
      C = 500        C = 500      P
      D = 600        D = 600      P
      Message = "Submenu 1"    Message = ""    FAIL
    ---
    Invoking test case 3 … Failed
    Expected values:      Actual values:
      A = 0        A = 200      FAIL
      B = 0        B = 300      FAIL
      C = 500        C = 500      P
      D = 600        D = 600      P
      Message = ""      Message = "Main Menu"  FAIL
    ---
    <…>
    Invoking test case 200 … Passed
      Message = "Submenu 1"    Message = "Submenu 1  P
      Prompt  = ">"      Prompt = ">"    P
    ---
    Final Stats
    180 test cases passed
    20 test cases failed
    200 test cases total
  • Более детально различные форматы отчетов о тестировании будут рассмотрены позднее, а пока остановимся более подробно на важном критерии оценки качества системы тестов и степени полноты тестирования системы - уровне покрытия программного кода тестами.

    6.3. Покрытие программного кода

    6.3.1. Понятие покрытия

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

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

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

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

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

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

    6.3.2. Уровни покрытия

    6.3.3. По строкам программного кода (Statement Coverage)

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

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

    Например, для полного покрытия всех строк следующего участка программного кода на языке C достаточно одного тестового примера:

    Вход: condition = true; Ожидаемый выход: *p = 123.
    int* p = NULL;
    if (condition)
        p = variable;
    *p = 123;

    Даже если в состав тестов не будет входить тестовый пример, проверяющий работу фрагмента при значении condition = false, код будет покрыт. Однако, в случае condition = false выполнение фрагмента вызовет ошибку.

    Аналогичные проблемы возникают при проверке циклов do … while - при данном уровне покрытия достаточно выполнение цикла только один раз, при этом метод совершенно нечувствителен к логическим операторам || и

    Другой особенностью данного метода является зависимость уровня покрытия от структуры программного кода. На практике часто не требуется 100% покрытия программного кода, вместо этого устанавливается допустимый уровень покрытия, например 75%. Проблемы могут возникнуть при покрытии следующего фрагмента программного кода:

    if (condition)
       functionA();
    else
       functionB();

    Если functionA() содержит 99 операторов, а functionB() - один оператор, то единственного тестового примера, устанавливающего condition в true, будет достаточно для достижения необходимого уровня покрытия. При этом аналогичный тестовый пример, устанавливающий значение condition в false, даст слишком низкий уровень покрытия.

    6.3.3.1. По веткам условных операторов (Decision Coverage)

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

    Также данный метод называют: branch coverage, all-edges coverage, basis path coverage, DC, C2, decision-decision-path.

    В отличие от предыдущего уровня покрытия данный метод учитывает покрытие условных операторов с пустыми ветками. Так, для покрытия по веткам участка программного кода

    a = 0;
    if (condition) {
      a = 1;
    }

    необходимы два тестовых примера:

    1. Вход: condition = true; Ожидаемый выход: a = 1;
    2. Вход: condition = false; Ожидаемый выход: a = 0;

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

    if ( condition1  ( condition2 || function1() ) )
        statement1;
    else
        statement2;

    полное покрытие по веткам может быть достигнуто при помощи двух тестовых примеров:

    1. Вход: condition1 = true, condition2 = true
    2. Вход: condition1 = false, condition2 = true/false (любое значение)

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

    3. Вход: condition1 = true, condition2 = false.

    6.3.3.2. По компонентам логических условий

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

    6.3.3.3. Покрытие по условиям (Condition Coverage)

    Для обеспечения полного покрытия по данному методу каждая компонента логического условия в результате выполнения тестовых примеров должна принимать все возможные значения, но при этом не требуется, чтобы само логическое условие принимало все возможные значения. Так, например, при тестировании следующего фрагмента:

    if (condition1 | condition2)
      functionA();
    else
      functionB();

    для покрытия по условиям потребуется два тестовых примера:

    (1)  Вход: condition1 = true, condition2 = false
    (2)  Вход: condition1 = false, condition2 = true.

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

    6.3.3.4. Покрытие по веткам/условиям (Condition/Decision Coverage)

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

    Для покрытия рассмотренного выше фрагмента с условием condition1 | condition2 потребуется 2 тестовых примера:

    1.  Вход: condition1 = true, condition2 = true
    2.  Вход: condition1 = false, condition2 = false.

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

    6.3.3.5. Покрытие по всем условиям (Multiple Condition Coverage)

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

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

    Еще одним недостатком метода является зависимость количества тестовых примеров от структуры логического выражения. Так, для условий, содержащих одинаковое количество компонент и логических операций:

    a  b  (c || (d  e))
    ((a || b)  (c || d))  e

    потребуется разное количество тестовых примеров. Для первого случая для полного покрытия нужно 6 тестов, для второго - 11.

    6.3.4. Метод MC/DC для уменьшения количества тестовых примеров при 3-м уровне покрытия кода

    Для уменьшения количества тестовых примеров при тестировании логических условий фирмой Boeing был разработан модифицированный метод покрытия по веткам/условиям (Modified Condition/Decision Coverage или MC/DC) [25, 26]. Данный метод широко используется при верификации бортового авиационного программного обеспечения согласно процессам стандарта DO-178B [7].

    Для обеспечения полного покрытия по этому методу необходимо выполнение следующих условий:

  • каждое логическое условие должно принимать все возможные значения;
  • каждая компонента логического условия должна хотя бы один раз принимать все возможные значения;
  • должно быть показано независимое влияние каждой из компонент на значение логического условия, т.е. влияние при фиксированных значениях остальных компонент.
  • Покрытие по этой метрике требует достаточно большого количества тестов для того, чтобы проверить каждое условие, которое может повлиять на результат выражения, однако это количество значительно меньше, чем требуемое для метода покрытия по всем условиям. В таблице 6.1 приведены примеры тестовых наборов, необходимых для тестирования логических блоков по MC/DC. Так, например, для блока OR достаточно n+1 тестовых примеров, где n - количество входов логического блока. Первый тестовый пример показывает, что при нулевых значениях входов значение выхода также нулевое. В каждом из следующих n примеров значение каждого входа устанавливается в 1, чем показывается независимое влияние входов на значение выхода.

    Логические блоки и определенные для них тестовые наборы
    AND блок. Реализует логическую функцию NAND блок. Реализует логическую функцию
    № набора 1 2 3 4 ··· n + 1 № набора 1 2 3 4 ··· n + 1
    Вход 1T F T T ··· T Вход 1T F T T ··· T
    Вход 2T T F T ··· T Вход 2T T F T ··· T
    Вход 3T T T F ··· T Вход 3T T T F ··· T
    ······ ··· ··· ··· ··· ··· ······ ··· ··· ··· ··· ···
    Вход nT T T T ··· F Вход nT T T T ··· F
    ВыходT F F F ··· F ВыходF T T T ··· T
    OR блок. Реализует логическую функцию NOR блок. Реализует логическую функцию
    № набора 1 2 3 4 ··· n + 1 № набора 1 2 3 4 ··· n + 1
    Вход 1F T F F ··· F Вход 1F T F F ··· F
    Вход 2F F T F ··· F Вход 2F F T F ··· F
    Вход 3F F F T ··· F Вход 3F F F T ··· F
    ······ ··· ··· ··· ··· ··· ······ ··· ··· ··· ··· ···
    Вход nF F F F ··· T Вход nF F F F ··· T
    ВыходF T T T ··· T ВыходT F F F ··· F

    6.3.5. Анализ покрытия

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

  • анализ должен подтвердить, что полнота покрытия тестами структуры кода соответствует требуемому виду покрытия и заданному минимально допустимому проценту покрытия;
  • анализ полноты покрытия тестами структуры кода может быть выполнен с использованием исходного текста, если программное обеспечение не относится к уровню A. Для уровня А необходимо проверить объектный код, сгенерированный компилятором, и выяснить, трассируется ли он в исходный текст или нет. Если объектный код не трассируется в исходный текст, должны быть проведены поверки объектного кода на предмет правильности генерации последовательности команд. Примером объектного кода, который напрямую не трассируется в исходный текст, но генерируется компилятором, может быть проверка выхода за заданные границы массива;
  • анализ должен подтвердить правильность передачи данных и управления между компонентами кода.
  • Анализ полноты покрытия тестами может выявить часть исходного кода, которая не исполнялась в ходе тестирования. Для разрешения этого обстоятельства могут потребоваться дополнительные действия в процессе проверки программного обеспечения. Эта неисполняемая часть кода может быть результатом:

  • недостатков в формировании тестовых примеров или тестовых процедур, основанных на требованиях. В этом случае должен быть дополнен набор тестовых примеров или изменены тестовые процедуры для обеспечения покрытия упущенной части кода. При этом может потребоваться пересмотр метода (методов), используемого для проведения анализа полноты тестов на основе требований;
  • неадекватности в требованиях на программное обеспечение: В этом случае должны быть модифицированы требования на программное обеспечение, разработаны и выполнены дополнительные тестовые примеры и тестовые процедуры;
  • "мертвого" кода. Этот код должен быть удален, и необходимо провести анализ для оценки эффекта удаления и необходимости перепроверки;
  • дезактивируемого кода. Для дезактивируемого кода, который не предполагается к выполнению в каждой конфигурации, сочетание анализа и тестов должно продемонстрировать возможности средств, которыми непреднамеренное исполнение такого кода предотвращается, изолируется или устраняется. Для дезактивируемого кода, который выполняется только при определенных конфигурациях, должна быть установлена нормальная эксплуатационная конфигурация для исполнения этого кода, и для нее должны быть разработаны дополнительные тестовые примеры и тестовые процедуры, удовлетворяющие целям полноты покрытия тестами структуры кода;
  • избыточности условия. Логика работы такого условия должна быть пересмотрена. Например, в условии if (A B || !B) принципиально невозможно проверить, что часть условия A B будет равна False в случае, когда A=True и B=False, так как вторая часть условия (!B) будет равна True, и общий результат логического выражения будет True ;
  • защитного кода. Эта часть кода используется для предотвращения исключительных ситуаций, которые могут возникнуть в процессе работы программы. Как пример, это может быть ветка default в операторе выбора switch, причем входное условие оператора switch может принимать определенные значения, которые он описывает, и, как следствие, ветка default никогда не будет выполнена.
  • Вернуться к учебному плану