Рекомендация: Идеи тестов для вызовов методов
Идеи тестов опираются на вероятные сбои программного обеспечения и способы обнаружения этих сбоев. Эта рекомендация описывает способ выявления участков кода, где не проверяется результат вызова метода.
Взаимосвязи
Связанные элементы
Основное описание

Введение

Ниже приведен фрагмент ошибочного кода:

File file = new File(stringName);
file.delete();

Ошибка состоит в том, что вызов File.delete может быть не выполнен, но код не проверяет это. Исправить это можно следующим образом (код выделен курсивом):

File file = new File(stringName);



if (file.delete()


== false) {...}

Эта рекомендация описывает способ выявления участков кода, где не проверяется результат вызова метода. Обратите внимание, что при этом считается, что метод дает правильные результаты при любых входных данных. Это также должно тестироваться, но создание тестов для вызываемого метода - это отдельная задача. В ваши обязанности не входит тестирование File.delete.)

Основная идея состоит в том, что следует протестировать отличные, необработанные, значимые результаты вызова метода. Для того чтобы определить этот термин, сначала рассмотрим, что такое результат. При выполнении метода в мире что-то меняется. Некоторые примеры приведены ниже:

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

Рассмотрим, что означает значимые, опять на примерах.

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

Разница между значимым и незначимым не всегда однозначна. Метод print может приводить к выделению ресурсов для буфера, и это может быть значимым. Бывают ситуации, когда изъян возникает вследствие того, как и когда выделяются буферы. Это возможно, чаще всего маловероятно.

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

  • отрицательное число
  • записано байт ровно столько, сколько запрошено
  • записано байт меньше, чем запрошено.

Все отрицательные значения объединяются в одну группу, потому что большинство программ не будет их различать. Все они (сколько бы вариантов ни было) будут считаться ошибкой. Если код должен записать 500 байт, то ему все равно, записано 34 или 340, так или иначе, для оставшихся байт запись нужно повторить. Если, скажем, для 0 должно быть выполнено другое действие, то это выделит еще один отличный результат.

Остается объяснить последний термин в определении. В данной методике тестирования нас не интересуют результаты, которые уже были обработаны. Рассмотрим код:

File file = new File(stringName);
if (file.delete() == false) {...}

Два отличные результата - это true и false. Код их обрабатывает. Правильно ли он это делает, проверяют идеи тестов, описанные в разделе Рекомендация по рабочему продукту: идеи тестов для булевских выражений и граничных условий. Эта методика посвящена отличным результатам, которые не обрабатываются специальным кодом. Это может иметь место по двум причинам: вы посчитали различие незначимым, или просто его не заметили. Пример первого случая:

result = m.method();
switch (result) {
    case FAIL:
    case CRASH:
       ...
       break;
    case DEFER:
       ...
       break;
    default:
       ...
       break;
}

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

result = s.shutdown();
if (result == PANIC) {
   ...
} else {
   // готово. Выключить реактор.
   ...
} 

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

Поиск идей тестов

Итак, ваша задача - найти ранее не замеченные значимые отличные результаты. Это кажется невозможным: как можно посчитать значимым то, что ранее было посчитано незначимым?

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

"Невероятные" случаи

Часто кажется, что какая-либо ошибка возникнуть не может. Проверьте, так ли это.

В этом примере код на Java обрабатывает временные файлы в Unix.

File file = new File("tempfile");
FileOutputStream s;
try {
    // открыть временный файл.
    s = new FileOutputStream(file);
} catch (IOException e) {...}
// удалить временный файл
file.delete();

Цель его состоит в том, чтобы временные файлы всегда удалялись, независимо от того, как заканчивает работу программа. Это достигается тем, что после создания временного файла он будет удален. В Unix удаленный файл будет удален операционной системой в любом случае после завершения процесса. Беспечный программист Unix может полениться написать код, проверяющий, был ли удален файл. Раз файл был успешно создан, он будет и удален.

Этот прием не сработает в Windows. Файл не будет удален, потому что он открыт. Обнаружить этот факт сложно: в августе 2000 в документации Java не были перечислены ситуации, в которых delete может не сработать, такая возможность лишь упоминалась. Но "в режиме тестирования" программист может заметить такую возможность. Поскольку код должен работать везде, то программист обратится к специалисту в Windows и спросит, когда File.delete и обнаружит, что в Windows все будет плохо.

"Незначимые" случаи

Другая возможность не заметить ошибку - посчитать ее несущественной. В Java Comparator содержит метод compare , который возвращает число <0, 0 или >0. Это три различных варианта. В следующем коде два из них смешаны в одну кучу:

void allCheck(Comparator c) {
   ...
   if (c.compare(o1, o2) <= 0) {
      ...
   } else {
      ...
   }

Это может быть ошибкой. Для того чтобы обнаружить это, следует проверить два случая отдельно, даже если кажется, что они одинаковые. Протестируйте свои представления. Если блок then в if выполняется несколько раз, почему бы не прогнать код с данными, когда результат меньше нуля и когда он равен нулю?

Необрабатываемые исключительные ситуации

Исключительные ситуации являются различными результатами. Рассмотрим код:

void process(Reader r) {
   ...
   try {
      ...
      int c = r.read();
      ...
   } catch (IOException e) {
      ...
   }
}

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

void process(Reader r) 


throws IOException {
    ...
    int c = r.read();
    ...
}

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

void process(Reader r) throws IOException {
    ...
    


Tracker.hold(this);
    ...
    int c = r.read();
    ...
    


Tracker.release(this);
    ...
}

Здесь код влияет на глобальное состояние посредством Tracker.hold). Если генерируется исключительная ситуация, то Tracker.release не будет вызван вообще.

Обратите внимание, что отсутствие вызова метода release может не привести ни к каким очевидным последствиям. Неполадка возникнет только после повторного вызова process когда попытка заблокировать объект повторно не будет успешной. Такие ошибки хорошо описаны в статье "Testing for Exceptions".   (Получить Adobe Reader))

Необнаруженные ошибки

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

Неверные аргументы

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

... strncmp(s1, s2, strlen(s1)) ...
... strncmp(s1, s2, strlen(


s2)) ...

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

В этом случае требуется три теста для трех вариантов возвращаемых значений. Ниже они перечислены:

s1 s2 ожидаемый результат фактический результат
"a" "bbb" <0 <0
"bbb" "a" >0 >0
"foo" "foo" =0 =0

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

s1 s2 ожидаемый результат фактический результат
"foo" "food" <0 =0

Хотя для обнаружения таких ошибок существуют свои методики, они редко применяются на практике. Лучше сосредоточить усилия на более широком круге тестов, позволяющих выявить много типов ошибок (в надежде на то, что и эта ошибка будет так или иначе обнаружена).

Нечеткие результаты

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

void connect() {
   ...
   Integer portNumber = serverPortFromUser();
   if (portNumber == null) {
      // показать сообщение о неверном номере порта
      return;
   }

Для получения номера порта он вызывает serverPortFromUser. Этот метод возвращает два отличающихся значения. Он возвращает номер порта, указанного пользователем, если этот порт допустим (1000 или более). В противном случае он возвращает null. Если возвращается null, то код выдает сообщение об ошибке и завершает работу.

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

Код для serverPortFromUser чуть более сложный. Сначала в всплывающем окне запрашивается строка, и окно имеет кнопки OK и Отмена. В зависимости от действий пользователя возможны четыре варианта:

  1. Если пользователь вводит правильное число, возвращается это число.
  2. Если номер меньше 1000, то возвращается null, и показывается сообщение о неверном номере порта.
  3. Если номер указан в неверном формате, возвращается null и показывается сообщение о неверном номере порта.
  4. Если пользователь нажимает кнопку Отмена, возвращается null.

Это код также работает правильно.

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

Проблема состоит в том, что null здесь представляет два разных исхода ("неверное значение" и "отмену пользователем"). Ничего в этой методике не побуждает вас обнаружить эту неполадку в проектировании serverPortFromUser.

Тестирование сможет помочь в решении проблемы. При тестировании serverPortFromUser как такового можно обнаружить, что код возвращает правильное значение во всех четырех вариантах ситуации, но при этом утрачивается контекст. Вместо этого следует протестировать connect. В четырех вариантах будут протестированы оба метода совместно:

ввод ожидаемый результат задуманный процесс
пользователь вводит "1000" открывается соединение с портом 1000 serverPortFromUser возвращает используемый номер.

пользователь вводит "999"

сообщение о недопустимом номере порта

serverPortFromUser возвращает null, и показывается окно

пользователь вводит "i99"

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

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