Содержание статьи
При разработке любого относительно сложного программного комплекса (особенно при разработке систем, выполняющих сложные расчеты) программист большую часть времени тратит не на написание программного кода, а на его отладку. О том, как потратить это время с пользой мы сегодня тебе расскажем.
В отладке программного обеспечения действует так называемый принцип Парето — в данном случае он может быть сформулирован как
"На отладку 20%-ного кода уходит 80% общего времени отладки". Попробуем разобраться, почему так происходит. Предположим, существует программа, выполняющая некоторые расчеты. Также предположим, что в изначальной версии программы третий метод не был реализован (и, соответственно, не был учтен при проектировании архитектуры ПО).
Однако в следующей версии программы понадобилось его добавить.
Как видно из блок-схемы, третий метод очень трудно отлаживается, и в случае, если программист допустит ошибку, найти ее при отладке будет очень сложно, так как из-за условного оператора этот метод будет вызываться крайне редко. Одним из выходов может быть написание небольшой отдельной программы для тестирования этого метода. Однако он не профессионален и зачастую слишком сложен — к примеру, если этот метод зависит от других классов. Для того чтобы избежать таких проблем, следует использовать модульное тестирование.
Модульное тестирование (англ. unit testing) — один из методов тестирования программного обеспечения, при котором пишется отдельный набор тестов для каждого класса, состоящий из тестов для каждого метода, объявленного в тестируемом классе. Подобная техника заменяет большую часть отладки и значительно упрощает разработку приложения как на конечных, так и на начальных стадиях, потому что позволяет проверить все случаи поведения каждого метода тестируемого класса, что зачастую не представляется возможным при
"ручной" отладке.
К примеру, если требуется перебрать все значения у аргумента функции типа int32, — у модульного теста это займет несколько минут, а тестировщик вряд ли вообще сможет справиться с этим заданием. Для еще большего упрощения и ускорения разработки рекомендуется использовать методологию разработки, называемую
"Разработка через тестирование". Разработка через тестирование, или TDD (англ. Test Driven Development), — один из видов экстремального программирования. Если в классических методах разработки сначала пишется программный код, а потом (при условии использования модульных тестов) — тесты, то в TDD сначала пишутся модульные тесты, и только потом тестируемые классы и методы реализуются в программном коде. Несмотря на то, что этот метод относится к экстремальному программированию, и, на первый взгляд, кажется абсурдным, он все чаще используется при разработке крупных программных продуктов.
Требования, предъявляемые к программному коду при разработке через тестирование
- Код должен быть разделен на как можно более мелкие части
- Должен выполняться принцип "один тест — одно действие", то есть один test case не должен проверять правильность выполнения более чем одного действия. Инициализация объектов должна производиться вне тестов.
- Желательно выполнение принципа "один тест — один метод", то есть test case должен содержать 1-2 строки кода.
- Должны быть соблюдены уровни абстракции классов программы, то есть логика программы не должна быть привязана к интерфейсу программы.
Преимущества разработки через тестирование
- Отделение логики программного продукта от интерфейса пользователя
- Как следствие из предыдущего пункта — упрощение процедуры повторного использования кода в других программных продуктах
- Упрощение отладки, поддержки и доработки программного кода за счет разделения его на небольшие части
- Меньшая вероятность неожиданного поведения программы
Роберт Мартин, известный специалист в области экстремального программирования, предлагает использовать следующий алгоритм TDD:
"Сначала нужно добиться, чтобы код хоть как-то работал, и только потом улучшать его". На практике эта рекомендация выглядит следующим образом:
- Написать модульный тест для какого-либо метода (на данный момент еще не реализованного). Вследствие того, что метод еще не реализован, тестовый проект даже не скомпилируется.
- Написать "заглушку" для этого метода. К примеру, если метод должен возвращать переменную типа boolean,
то он должен содержать только строку вида "return false" (в случае, если при правильном выполнении метода должен быть возвращен true), то есть заглушка должна возвращать такое значение, чтобы тест
"не проходил". Теперь тестовый проект компилируется, но тест по понятной причине не выполняется. - Реализовать метод алгоритмически правильно, но не пытаться улучшить его — требуется просто сделать
"чтобы работал". Убедиться, что все тесты проходят. - Усовершенствовать код — привести к наиболее удобочитаемому виду, разбить метод на более мелкие части. Убедиться, что все тесты проходят.
- Перейти к реализации следующего метода.
Когда не рекомендуется использовать модульные тесты?
В задачах, выполняющихся слишком долго. К примеру, метод, выполняющий запрос к БД, лучше исключить из списка тестируемых, потому что все тесты должны выполняться каждый раз при запуске тестирования, и если тест выполняется долго — программист будет стремиться отключить его. Если нужно протестировать метод, разбирающий ответ от БД, то лучше отделить этот метод непосредственно от запроса, и передавать ему заранее подготовленные
"фальшивые" данные.
Как это выглядит на практике?
В качестве примера мы напишем приложение (и модульные тесты к нему) для ОС Android. Этот пример частично актуален не только для Android, но и для любой платформы, поддерживающей Java. Тестовое приложение будет принимать от пользователя массив точек и принимать решение, расставлены ли они в правильном порядке, возвращая true или false. Логика расстановки проясняется, если представить, что на первой точке написано, к примеру,
"1", на второй — "2", на третьей — "3" и т.д.
Точки могут располагаться в ряд, в столбик или в смешанном порядке.
Для начала нужно создать проект для Android (подразумевается, что у тебя уже установлена среда разработки, к примеру, Eclipse, а также плагин ADT и Android SDK)
и тестовый проект. Для того чтобы создать тестовый проект, нужно в диалоге
создания проекта нажать кнопку "Next" и отметить чекбокс "Create a test project".
Теперь можно приступить к написанию модульного теста. В тестировании Java-приложений стандартом де-факто считается JUnit. JUnit также включен в Android SDK, соответственно, тестирование приложений на Android производится именно при помощи этой библиотеки.
Тестирование логики
Сначала создается TestSuite для проекта (его код модифицировать не нужно), а потом — TestCase для каждого класса. В нашем случае класс всего один, и в нем содержится всего один открытый метод (статический). Напишем два теста — первый будет передавать заведомо верный список точек, второй — заведомо неверный.
public void testValidOrder()
{
List<Point> points = new ArrayList<Point>();
points.add(new Point(0, 0));
points.add(new Point(1, 0));
points.add(new Point(2, 0));
points.add(new Point(3, 0));
boolean result = Matrix.orderIsRight(points);
assertTrue(result);
}
public void testInvalidOrder()
{
List<Point> points = new ArrayList<Point>();
points.add(new Point(0, 0));
points.add(new Point(3, 0));
points.add(new Point(1, 0));
points.add(new Point(2, 0));
boolean result = Matrix.orderIsRight(points);
assertFalse(result);
}
Прошу обратить внимание, что каждый тест должен начинаться со слова test, иначе он не будет распознан JUnit как… ну, в общем, как тест :). Чтобы запустить выполнение тестов, нужно нажать <Ctrl> + <F11>.
Однако тесты даже не запустятся, потому что класс Matrix не содержит метода orderIsRight().
Теперь следует написать заглушку для этого метода, состоящую из одной строчки: "return false". Тесты запустятся, но их поведение будет немного странным: первый тест не будет пройден, а второй — будет. Сначала нужно добиться прохождения первого теста, и только потом браться (если, конечно, потребуется) за второй.
Реализуем метод orderIsRight() следующим образом:
public static boolean orderIsRight(
final List<Point> pPoints)
{
Point firstPoint = pPoints.get(0);
for (int i = 1; i < pPoints.size(); i++)
{
final Point secondPoint = pPoints.get(i);
if (pointsAreInWrongOrder(fi rstPoint,
secondPoint)) {
return false;
}
firstPoint = secondPoint;
}
return true;
}
Метод pointsAreInWrongOrder() — закрытый статический, состоящий из одной строки:
return (pFirstPoint.x >= pSecondPoint.x);
Нетрудно догадаться, что он возвращает true, если первая точка находится правее, чем вторая (либо перекрывает ее). Запускаем тесты — они проходят. Отлично, на этом тестирование логики приложения можно считать законченным. Теперь можно приступить к тестированию графического интерфейса.
Тестирование GUI
Графический интерфейс — крайне важная часть любого ПО. Если пользователю не понравится GUI — он, скорее всего, не будет использовать программу, какая бы мощная
"начинка" не содержалась внутри. Тестирование GUI должно быть не менее тщательным, чем тестирование логики программы, причем тестирование желательно максимально автоматизировать — только так можно покрыть наибольшее количество возможных вариантов действий пользователя. Конечно же, программист может протестировать интерфейс вручную, но здесь есть один нюанс. Дело в том, что программист тестирует приложение с точки зрения программиста, а не пользователя, и ему, в отличие от пользователя, может просто не прийти в голову, что можно ввести символьную строку в поле для ввода чисел.
В Android SDK есть инструментарий для тестирования графического интерфейса, в который включен класс ActivityInstrumentationTestCa se2. Его следует наследовать при написании модульных тестов для GUI. Стоит обратить внимание на то, что ActivityInstrumentationTest Case2 — это шаблонный класс, т.е. использовать его следует в виде class MainActivityTest extends ActivityInstrumentationTestCase2<Mai nActivity>.
Наше приложение содержит одну Activity (MainActivity), ее мы и будем тестировать. Как она выглядит, можно увидеть на рисунке на предыдущей странице.
Я использую отладку на устройстве с Android 2.3.4 через Wi-Fi. Кстати говоря, отладка по Wi-Fi очень удобна по сравнению с отладкой по кабелю и, тем более, в эмуляторе. Для того чтобы отлаживать приложения подобным образом, нужно установить виджет Adb over Wi-Fi из Android Market (подходит только для телефонов с правами root). После запуска нужно подключиться к устройству. Например, следующим образом:
adb connect 192.168.1.5:31337
Тестирование GUI выглядит приблизительно так же, как и тестирование логики приложения. Создается класс, в нем создается метод setUp(), в котором инициализируются объекты. Объекты, представляющие Activity, поля для ввода текста и кнопку следует сделать закрытыми полями:
private Activity mActivity;
private EditText mEditText1;
private EditText mEditText2;
private EditText mEditText3;
Инициализируются они следующим образом:
protected void setUp() throws Exception
{
super.setUp();
mActivity = getActivity();
mEditText1 = (EditText)mActivity.findViewById
(com.example.matrix.R.id.editTextLine1);
<...>
mTextView = (Button)mActivity.findViewById(com.example.matrix.R.id.textView);
}
В первую очередь нужно проверить, создались ли элементы интерфейса:
public void testControlsCreated()
{
assertNotNull(mActivity);
assertNotNull(mEditText1);
<...>
assertNotNull(mTextView);
}
Этот тест проходит без ошибок, но лучше перестраховаться и выполнять его всегда — он может не проходить, если, к примеру, у какого-либо элемента неправильно задано какое-то свойство. В полях для ввода указываются координаты точки через пробел, а в textView появляется результат (OK или NOT OK).
Напишем модульный тест для просчета:
public void testValidData()
{
TouchUtils.tapView(this, mEditText1);
sendKeys(KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_SPACE,
KeyEvent.KEYCODE_0);
TouchUtils.tapView(this, mEditText2);
sendKeys(KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_SPACE,
KeyEvent.KEYCODE_0);
TouchUtils.tapView(this, mEditText3);
sendKeys(KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_SPACE,
KeyEvent.KEYCODE_0);
TouchUtils.tapView(this, mEditText1);
assertEquals("OK", mTextView.getText());
}
Он не проходит, потому что редактирование не обрабатывается. Напишем обработчик для события смены фокуса на EditText’ах — и тест будет пройден. Код обработчика приводиться не будет из-за ограниченности объема статьи, к тому же, он абсолютно тривиален. Также необходимо проверить, правильно ли обрабатываются неверные данные - это задание останется на твоей совести, тем более, что оно практически аналогично предыдущему тесту :).
При запуске тестов можно наблюдать, как сами нажимаются элементы интерфейса, переключаются Activity (для каждого теста Activity запускается заново) — забавное зрелище :).
Мораль сей басни такова
В данной статье были рассмотрены как теоретическая, так и практическая части разработки через тестирование. В качестве примера приведен код приложения на Java под ОС Android с использованием инструментария JUnit, однако принципы тестирования приблизительно одинаковы на любой ОС.
Конечно же, разработка через тестирование не исключает тестирования приложения из цикла разработки, однако существенно облегчает как последующее тестирование, так и разработку в целом, позволяя избежать досадных ошибок, на исправление которых уходят многие часы.