Альтернативой шаблону «Тестовые данные» (Test Data) является шаблон «Реалистичные данные» (Realistic Data), в рамках которого для тестирования используются данные из реального мира. Реалистичные данные удобно применять в следующих ситуациях:
• вы занимаетесь тестированием системы реального времени, используя цепочки внешних событий, которые возникают в реальных условиях эксплуатации;
• вы сравниваете вывод текущей системы с выводом предыдущей системы (параллельное тестирование);
• вы выполняете рефакторинг кода, имитирующего некоторый реальный процесс, и ожидаете, что после рефакторинга результирующие данные будут в точности такими же, как до рефакторинга, в особенности если речь идет о точности операций с плавающей точкой.
Каким образом в тесте можно отразить назначение тех или иных данных? Добавьте в тест ожидаемый и реально полученный результат и попытайтесь сделать отношение между ними понятным. Вы пишете тесты не только для компьютера, но и для читателя. Через несколько дней, месяцев или лет кто-нибудь будет смотреть на ваш код и спрашивать себя: «Что имел в виду этот шутник, когда писал этот запутанный код?» Попробуйте оставить своему читателю как можно больше подсказок, имейте в виду, что этим разочарованным читателем можете оказаться вы сами.
Вот пример. Если мы конвертируем одну валюту в другую, мы берем комиссию 1,5 за выполнение операции. Представьте, что мы обмениваем американские доллары (USD) на британские фунты стерлингов (GBP). Пусть курс обмена будет составлять 2:1. Если мы хотим обменять $100, в результате мы должны получить 50 GBP – 1,5 % = 49,25 GBP. Мы могли бы написать следующий тест:
Bank bank = new Bank().
bank.addRate("USD", "GBP", STANDARD_RATE);
bank.commission(STANDARD_COMMISSION);
Money result = bank.convert(new Note(100, "USD"), "GBP");
assertEquals(new Note(49.25, "GBP"), result);
Однако вместо этого мы можем сделать порядок вычислений более очевидным:
Bank bank = new Bank();
bank.addRate("USD", "GBP", 2);
bank.commission(0.015);
Money result = bank.convert(new Note(100, "USD"), "GBP");
assertEquals(new Note(100 / 2 * (1–0.015), "GBP"), result);
Прочитав этот тест, я вижу взаимосвязь между входными значениями и значениями, используемыми в составе формулы.
Шаблон «Понятные данные» (Evident Data) обладает побочным эффектом: он в некоторой степени облегчает программирование. После того как мы в понятной форме записали выражение assert, мы получаем представление о том, что именно нам необходимо запрограммировать. В данном случае мы видим, что тестируемый код должен содержать операции деления и умножения. Мы даже можем воспользоваться шаблоном «Поддельная реализация» (Fake It), чтобы узнать, где должна располагаться та или иная операция.
Шаблон «Понятные данные» (Evident Data) выглядит как исключение из правила о том, что в коде не должно быть «магических» чисел. Дело в том, что в рамках одного метода легко понять назначение того или иного числа. Однако если в программе уже имеются объявленные символьные константы, я предпочитаю использовать их вместо конкретных численных значений.
26. Шаблоны красной полосы
В данной главе речь пойдет о шаблонах, которые подскажут вам, когда писать тесты, где писать тесты и когда прекратить писать тесты.
Какой следующий тест лучше всего выбрать из списка задач для реализации? Выбирайте тест, который, во-первых, научит вас чему-либо, а во-вторых, который вы сможете реализовать.
Каждый тест должен соответствовать одному шагу в направлении к вашей основной цели. Взгляните на этот список тестов и попробуйте определить, какой тест лучше всего выбрать в качестве следующего для реализации:
плюс;
минус;
умножение;
деление;
сложение с такой же валютой;
равенство;
равенство нулю;
нулевой обмен;
обмен одной и той же валюты;
обмен двух валют;
курс кросс-обмена.
Не существует единственно правильного ответа. То, что для меня, ни разу не занимавшегося реализацией этих объектов, будет выглядеть как один шаг, для вас, обладающих достаточным опытом, может оказаться одной десятой шага. Если вы не можете найти в списке тест, соответствующий одному шагу, добавьте в список дополнительные тесты, реализация которых поможет вам приблизиться к реализации тестов, уже присутствующих в списке.
Когда я смотрю на список тестов, я рассуждаю: «Это очевидно, это очевидно, об этом я не имею ни малейшего представления, это очевидно, здесь – никаких идей, о чем я думал, когда писал это? А! Вспомнил! Я думаю, что мог бы это сделать». Этот последний тест я реализую следующим. С одной стороны, он не кажется мне очевидным, с другой стороны, я уверен в том, что смогу заставить его работать.
Программа, выросшая из подобных тестов, может быть написана в рамках нисходящего подхода (сверху вниз), так как вы можете начать с теста, который ориентирован на вариант полного вычисления. Программа, выросшая из тестов, может быть написана и в рамках восходящего подхода (снизу вверх), так как вы начинаете с небольших кусочков и собираете их в конструкцию постепенно увеличивающегося размера.
И нисходящий, и восходящий подходы не представляют реального описания процесса. Во-первых, вертикальная метафора – это упрощенная визуализация процесса изменения программы в течение разработки. Для описания процесса разработки, основанной на тестировании, лучше подходит метафора Развития или Эволюции: внешняя среда влияет на программу, а программа влияет на внешнюю среду. Во-вторых, если мы хотим, чтобы в нашей метафоре присутствовало направление, лучшим описанием будет «от известного к неизвестному». Подразумевается, что мы обладаем некоторыми знаниями и опытом и ожидаем, что в процессе разработки мы будем узнавать нечто новое. Объединим эти две метафоры и получим, что программа эволюционирует от известного к неизвестному.
С какого теста следует начать разработку? Начните с тестирования варианта операции, который не подразумевает выполнения каких-либо осмысленных действий, то есть ничего не делает.
Приступая к реализации операции, вы прежде всего должны ответить на вопрос: «Где она должна располагаться?» Пока вы не ответите на этот вопрос, вы не будете знать, какой код необходимо написать, чтобы протестировать эту операцию. Как уже неоднократно рекомендовалось, не следует решать несколько проблем одновременно. Значит, вы должны выбрать такой тест, который позволит вам искать ответ только на один этот вопрос и на время забыть обо всех остальных вопросах.
Если вы с самого начала приступите к реализации реалистичного теста, вам придется искать ответы на несколько вопросов одновременно:
• Где должна располагаться операция?
• Какие входные данные считать корректными?
• Каким должен быть корректный результат выполнения операции при использовании выбранных входных данных?
Если вы начнете с реалистичного теста, вы слишком долгое время будете вынуждены действовать без обратной связи. Красный – зеленый – рефакторинг, красный – зеленый – рефакторинг. На выполнение этого цикла должно уходить всего несколько минут.
Но как сократить время цикла? Для этого вы можете воспользоваться тривиальными входными и выходными данными. Вот простой пример: если функция должна складывать многозначные вещественные числа с точностью до тысячного знака после запятой, вовсе не обязательно начинать ее реализацию с теста, проверяющего результат сложения таких огромных чисел. Вполне можно начать с тривиального теста 3 + 4 = 7. Вот еще один пример. В группе электронных новостей, посвященной экстремальному программированию, один из участников поинтересовался, как написать программу минимизации количества полигонов (многоугольников), составляющих некоторую поверхность. На вход подается набор полигонов, комбинация которых представляет собой некоторый трехмерный объект. На выходе должна получиться комбинация полигонов, которая описывает точно такой же объект (поверхность), но включает в себя минимальное возможное количество полигонов. «Как я могу разработать подобную программу, если для того, чтобы заставить тест сработать, я должен быть как минимум доктором наук?»
Используя шаблон «Начальный тест» (Starter Test), мы получаем ответ:
• Вывод должен быть точно таким же, как ввод. Некоторые комбинации полигонов изначально являются минимальными.
• Ввод должен быть как можно меньшего размера. Например, единственный полигон или даже пустой список полигонов.
Мой начальный тест выглядел следующим образом:
Reducer r = new Reducer(new Polygon());
assertEquals(0, reducer.result(). npoints);
Отлично! Первый тест заработал. Теперь можно перейти к остальным тестам в списке…
К начальному тесту следует применить рассмотренное ранее правило «Тест одного шага» (One Step Test): самый первый тест должен научить вас чему-то новому, кроме того, вы должны обладать возможностью достаточно быстро заставить его работать. Если вы реализуете подобный код уже не в первый раз, вы можете выбрать начальный тест для одной или даже двух операций. Вы должны быть уверены, что сможете быстро заставить тест работать. Если вы приступаете к реализации чего-либо достаточно сложного и делаете это впервые, начните с самого простого теста, который вы только можете представить.
Я часто замечаю, что мой начальный тест работает на достаточно высоком уровне и скорее напоминает тест всего приложения. Например, простой сетевой сервер. Самый первый тест выглядит следующим образом:
StartServer
Socket= new Socket
Message = "hello"
Socket.write(message)
AssertEquals(message, socket.read)