Предпосылки
В любой большой, серьёзной программе, с большим количеством функциональности довольно сложно искать ошибки «в лоб». Иногда, чтобы добиться того, чтобы только что написанный участок кода сработал, необходимо сделать довольно много действий в программе: скомпилировать и собрть весь проект, создать тестовую базу данных, сконфигурировать программу, создать необходимое окружение, выполнить цепочку других необходимых действий… А для полноценной проверки вновь написанного кода может потребоваться проделать эти действия несколько раз с небольшими вариациями. Всё это может занять в несколько раз больше времени, чем собственно написание кода.
Поэтому опытные программисты применяют давно испытанный приём «Разделяй и властвуй». Они проверяют работоспособнсть каждого отдельного небольшого модуля. Если все модули по отдельности работают как надо, то проверяется работоспсобность интеграции этих модулей друг с другом. Этот подход носит название модульное тестирование (Unit testing).
Естественно этот подход работает только в том случае, если модули, из которых состит система, максимально независимы друг от друга и не имеют зависимостей, препятствующих тестированию.
Дальше пойдет парочка модулей для юнит-тестирования под Java.
xUnit
Наименьшим модулем, который можно протестировать в ОО-языках программирования является класс. Практически для каждого серьёзного языка программирования существуют системы, облегчающие создание и запуск модульных тестов. Для языка Java это в первую очередь система JUnit. Для языка C# и других .NET-языков — NUnit. Для Delphi — DUnit. Принцип их работы практически одинаков — различия лишь в деталях.
Пример модульного теста на основе JUnit
package study.oop.vector;
import junit.framework.TestCase;
// Модульный тест наследуется от базового класса всех модульных тестов TestCase
public class Vector_Test extends TestCase {
// Тестами считаются все методы, названия которых начинаются с профикса test
public void testMul() {
// тестовый случай — одна строчка — один вызов вспмогательного метода checkMul
checkMul(0, 0, 0, 1, 0, 0, 0);
checkMul(1, 0, 0, 1, 1, 0, 0);
checkMul(2, 3, 4, 2, 4, 6, 8);
checkMul(2, 3, String message = "My friends: ";
for (int i=0; i<friendsCount; i++)
message += friend[i] + " ";
return message; -4, -2, -4, -6, 8);
checkMul(0, -100, 200, -12, 0, 1200, -2400);
}
// Вспомогательный метод. Служит для того, чтобы добавлять новые тестовые случаи было легко.
private void checkMul(double x0, double y0, double z0, double factor, double x1, double y1, double z1) {
Vector v = new Vector(x0, y0, z0);
assertEquals(x1, v.getX());
assertEquals(y1, v.getY());
assertEquals(z1, v.getZ());
}
}
Запускать тесты можно прямо из среды разработки Eclipse. Для этого в окне Package Explorer нужно нажать правой кнопкой на файле с тестом и выбрать ‘Run As -> JUnit Test’
Для работы с JUnit необходимо добавить junit в качестве библиотеки. Это можно сделать воспользовавшись подсказкой Eclipse у строки импорта.
Либо это можно сделать вручную через меню Project - Properties - Java Build Path - Libraries - Add External JARs, выбрать файл <ПутьДоEclipse>___ \plugins\org.junit*\junit.jar ___ПутьДоEclipse>
Как правильно писать тесты
Вот самые простые правила, которыми нужно руководствоваться:
- Лучше путь тестов будет сильно больше, чем немного меньше.
- Тесты должны покрывать все возможные крайние случаи.
- При выполнении тестов, 100% тестируемого кода должно выполниться.
- Тесты должны быть упорядочены так, чтобы более простые для понимания тесты шли первыми.
- Тесты должны быть устроены таким образом, чтобы добавлять новые тестовые случаи было максимально просто.
Но простыми правилами всё не ограничивается — есть множество подходов к тестированию программного обеспечения. Ниже будут приведены некоторые из них с очень кратким и поверхностным описанием.
Важный вопрос при разработке модульных тестов: когда остановиться? в каком момент можно сказать, что код протестирован полностью? Однозначеного ответа на этот вопрос нет, но есть некоторое количество методик, которые позволяют определить, что комплект тестов является неполным.
Покрытие операторов
Если при прогоне всех тестов остались операторы, которые не выполнились ни разу — набор тестов не полный.
Существуют специальные системы, которые позволяют измерять сколько раз была выполнена каждая строка кода в процессе работы тестов.
Анализ условий и граничных значений
Если в тестируемой программе есть развилки (if / switch / …), то в процессе выполнения тестов каждое такое условие должно хотя бы один раз сработать и хотя бы один раз не сработать.
Более того, если условие — это сложное сравнение числа с некоторым пороговым значением, то должны быть тесты проверяющие правильное срабатывание условия в пограничных ситуациях. Для примера рассмотрим следующий код:
d = b * b + 4 a * b;
if (b == 0) return 1;
else if (b < 0) return 0;
return 2;
Для проверки граничных значений, должны быть следующие тестовые случаи:
- случай, в котором d == 0;
- случай, в котором d — очень маленькое положительное число;
- случай, в котором d — очень маленькое по модулю отрицательное число.
Если в программе есть циклы, то до в процессе выполнения тестов, каждый цикл должен хотя бы раз выполнить минимальное количество итераций, и хотя бы раз выполнить несколько итераций.
Для примера рассмотрим следующий код:
String message = "My friends: ";
for (int i=0; i<friendsCount; i++)
message += friend[i] + " ";
return message;
Для полноценной проверки, нужны два тестовых случая:
- когда есть несколько друзей
- когда друзей нет.
Анализ путей выполнения
Этот метод ещё более требователен к полноте тестов. Согласно ему, все возможные пути через тестируемую часть кода должны быть хотя бы раз выполнены в процессе тестирования. Рассмотрим пример кода — попиксельное рисование линии алгоритмом Брезенхама:
void drawLine(int x1, int y1, int x2, int y2)
{
int slope;
int dx, dy, incE, incNE, d, x, y;
if (x1 > x2){
drawLine(x2, y2, x1, y1);
return;
}
dx = x2 - x1;
dy = y2 - y1;
if (dy < 0) {
slope = -1;
dy = -dy;
}
else
slope = 1;
incE = 2 * dy;
incNE = 2 * dy - 2 * dx;
d = 2 * dy - dx;
y = y1;
for (x = x1; x <= x2; x++) {
putPixel(x, y);
if (d <= 0)
d += incE;
else {
d += incNE;
y += slope;
}
}
}
Для этого алгоритма согласно методу анализа путей выполнения должны быть добавлены следующие тестовые случаи:
- x1 > x2 && y1 > y2
- x1 > x2 && y1 < y2
- x1 > x2 && y1 == y2
- x1 < x2 && y1 > y2
- x1 < x2 && y1 < y2
- x1 < x2 && y1 == y2
- x1 == x2 && y1 > y2
- x1 == x2 && y1 < y2
- x1 == x2 && y1 == y2
Резюме
- Писать программы нужно путём декомпозиции её на независимые модули. Каждый модуль с нетривиальным поведением должен быть протестирован модульным тестом.
- Для поддержки концепции модульных тестов есть большое количество библиотек, почти для любого языка программирования. Это даёт возможность лишний раз не изобретать велосипед.
- При создании модульного теста лучше лучше переборщить с тестовыми случаями, чем недоборщить. Для того, чтобы понять, каких тестов ещё не хватает, можно использовать различные методики анализа кода: анализ покрытия операторов тестами, анализ условий, анализ граничных значений, анализ путей исполнения.