Кому стоит читать дальше?
"Если вам нравится лепить вместе куски кода, которые более или менее работают, и вы счастливы думать, что вам не придётся возвращаться к полученному в результате этого коду в дальнейшем - TDD (разработка через тестирование) не для вас" (с) Кент Бек [c.339]
Сказ "Жил да был программист 1С"
Жил да был программист 1С. Жил себе не тужил, работал в одной сибирской компании. И вроде всё хорошо, но что-то не давало ему покоя. То нелепая ошибка в продуктиве вылезет, то данные как-то криво пишутся. И закручинился программист, потерял покой.
Думал, думал, ничего не надумал и пошёл тропой серой да к мудрецам, на весь свет известным, старцам google да yandex. И рассказали они ему про явление небывалое, что в народе тестированием кличут. И возрадовался программист - "Вот оно, лекарство от кручины твоей".
Пошёл он к другому старцу, что в народе infostart кличут. И стал спрашивать у него программист, дескать, слышал я про явление небывалое, что в народе тестированием кличут. Расскажи, добрый человек, как мне его к своей разработке подвязать. И начал ему старец кучу терминов показывать: TDD, BDD, vannesa-behavior, vanessa-automation, add, xUnit, CI/CD и прочие.
И прикинул программист, сколько всего ему изучать придётся, испугался, ведь, как известно, "у страха глаза велики". И задумался он, как быть и что делать, и о результатах этих размышлений и последующих действий решил написать статью.
От сказки к реальности
Результатом размышлений и изысканий является одна простая мысль - для того, чтобы начать тестировать свой код, не нужно НИЧЕГО, кроме желания и среды разработки. И чтобы прийти к этой мысли, достаточно немного подумать о том, а что такое тест (в самом примитивном, простом смысле).
Тест - это алгоритм, который может проверить работоспособность другого алгоритма.
А если ещё упростить, то тест - это код, который проверяет работу кода.
Следовательно, "писать тесты" означает писать код, который будет проверять, как работает другой код. А теперь рассмотрим 2 практических примера на эту тему
Пример 1 - тестируем "привет, мир"
- Можно ли протестировать следующий код?
Сообщить("Привет мир");
В таком написании - можно, но не нужно. И вот почему:
- Цель теста - проверить работу собственного кода. Задачи проверять работу системных методов или методов других разработчиков не стоит (разве что, если вы не уверены в корректности результата).
Вопрос, а что в этом коде написано разработчиком? Это сообщение "Привет, мир". Вот для него и можно написать тест.
Функция Тест_ПроверитьКорректностьТекстаПриветМир()
Если ПолучитьТекст_ПриветМир() <> "Привет мир" Тогда
Сообщить("[X] Тест провален");
Иначе
Сообщить("[V] Тест пройден");
КонецЕсли;
КонецФункции
Функция ВыполнитьТест()
Тест_ПроверитьКорректностьТекстаПриветМир();
КонецФункции
Шаг 2 - делаем так, что тест проходил проверку и запускался
Функция ПолучитьТекст_ПриветМир()
Возврат "Привет мир!";
КонецФункции
Функция Тест_ПроверитьКорректностьТекстаПриветМир()
Если ПолучитьТекст_ПриветМир() = "Привет мир" Тогда
Сообщить("[V] Тест пройден");
Иначе
Сообщить("[X] Тест провален");
КонецЕсли;
КонецФункции
Функция ВыполнитьТест()
Тест_ПроверитьКорректностьТекстаПриветМир();
КонецФункции
Шаг 3 - Проводим рефакторинг, выделяя проверку в отдельный метод
Функция ПолучитьТекст_ПриветМир()
Возврат "Привет мир!";
КонецФункции
Функция Тест_ПроверитьКорректностьТекстаПриветМир()
Проверить(ПолучитьТекст_ПриветМир(), "Привет мир!");
КонецФункции
Функция Проверить(Значение1, Значение2)
Если Значение1 = Значение2 Тогда
Результат = "[V] Тест пройден";
Иначе
Результат = "[X] Тест провален";
КонецЕсли;
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = Результат;
Сообщение.Сообщить();
КонецФункции
Функция ВыполнитьТест()
Тест_ПроверитьКорректностьТекстаПриветМир();
КонецФункции
Пример 2 - Тестируем формирование JSON
Это рабочая задача, которую я решал в марте и апреле 2022 года. Естественно, упрощённая. Итак, надо написать выгрузку номенклатуры в JSON. Требуемый результат
{
code: "001"
}
Разработка без тестирования
Пробуем решить задачу, не используя никакие тесты. Обычно код решения выглядит примерно так (код содержит ошибки, которые проходят через компилятор - это сделано специально)
Обычный код выгрузки номенклатуры
Функция ВыгрузитьНоменклатуру(СсылкаНаНоменклатуру)
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| спрНоменклатура.Код КАК Код
|ИЗ
| Справочник.Номенклатура КАК спрНоменклатура
|ГДЕ
| спрНоменклатура.Ссылка = &Ссылка";
Запрос.УстановитьПараметр("Сылка", СсылкаНаНоменклатуру);
Выборка = Запрос.Выполнить().Выбрать();
данныеJSON = Новый Структура;
данныеJSON.Вставить("codе", "");
Пока Выборка.Следующий() Цикл
данныеJSON.code = Выборка.code;
КонецЦикла;
Возврат МагияПревращенияВJSON(ДанныеJSON);
КонецФункции
Как проверяется этот код (если вообще проверяется)? Делается запуск внешней обработки и в режиме отладки начинаются исправления ошибок (алгоритм ручного тестирования пишу по памяти, извините)
- Запуск 1 - вылетает ошибка "Не указан параметр запроса Ссылка"
- Исправляем опечатку `"Сылка" -> "Ссылка"`
- Запуск 2 - ошибка "поле code не найдено"
- Исправляем, присваивая правильный псевдоним поля в запросе
- Запуск 3 - ошибка "Ключ code не найден"?
- [начинаем нервничать, ведь код-то простой вроде] Да как так-то? А вы случайно в ключе букву е русскую написали... блин, ладно исправляем
- Запуск 4 - ошибки нет, но результат возврата - Неопределено
- ДА ЧТО НЕ ТАК?! А функция `МагияПревращенияВJSON(ДанныеJSON)` ничего не возвращает (там забыли написать возврат)
Пример глупый, но в том или ином виде подобная картина возникает у любого разработчика начального или даже среднего уровня. Отдельно отмечу, как подобные циклы перезапуска влияют на программиста:
- повышается нервозность, ведь ты уверен, что код элементарный и он должен работать, но вместо этого вылезают ошибки
- портится настроение, ведь надо исправлять свои собственные опечатки и ошибки, а это всегда действует угнетающе, **если** ты к этому не готов.
Теперь посмотрим, как то же самое разрабатывается и проверяется через тестирование
Разрабатываем с использованием тестирования
Любая разработка с тестированием начинается с того, что составляется "План тестирования", проще говоря, нужно определиться с тем, что, собственно, тестировать. Под текущую задачу требуется, как минимум, четыре теста:
Шаг 0 - Выписываем какие тесты нам нужны
1. Тест структуры, которая будет использоваться для формирования, JSON
2. Тест корректности результата МагияПревращенияВJSON(ДанныеJSON)
3. Тест работоспособности запроса данных из БД
4. Тест заполнения данных структуры
Важное примечание! Каждый тест должен тестировать какую-то одну функциональность и быть независимым от других тестов.
Применимо к нашему случаю - нельзя тестировать 4 разных вещи внутри одной функции, а значит для каждой операции будет отдельная процедура/функция.
Для проверки будем использовать уже написанную нами функцию Проверить() (см. пример 1).
Шаг 1 - Пишем тест "Тест структуры формирования JSON"
Пишем тест, который точно завершается ошибкой (предполагается, что метод СравнитьСтруктуры давно существует)
Функция Тест_СтруктураНоменклатурыДляВыгрузки()
ЭталонныеДанныеJSON = Новый Структура;
ЭталонныеДанныеJSON.Вставить("codе", "");
КлючиРавны = СравнитьСтруктуры(ЭталонныеДанныеJSON, Неопределено);
Проверить(КлючиРавны, Истина);
КонецФункции
Функция ВыполнитьТесты() Экспорт
Тест_СтруктураНоменклатурыДляВыгрузки();
КонецФункции
Пишем реализацию и сразу запускает тест на проверку
Функция СтруктураНоменклатурыДляВыгрузки()
данныеJSON = Новый Структура;
данныеJSON.Вставить("codе", "");
Возврат данныеJSON;
КонецФункции
Функция Тест_СтруктураНоменклатурыДляВыгрузки()
ЭталонныеДанныеJSON = Новый Структура;
ЭталонныеДанныеJSON.Вставить("codе", "");
КлючиРавны = СравнитьСтруктуры(ЭталонныеДанныеJSON, СтруктураНоменклатурыДляВыгрузки());
Проверить(КлючиРавны, Истина);
КонецФункции
Функция ВыполнитьТесты() Экспорт
Тест_СтруктураНоменклатурыДляВыгрузки();
КонецФункции
Если где-то у нас была опечатка, то шанс, что печатались в двух местах минимальна, поэтому если тут была ошибка - то она сразу вылезет
Шаг 2 - Пишем тест "Тест корректности результата МагияПревращенияВJSON(ДанныеJSON)"
Пишем второй тест, который закончится ошибкой
Функция Тест_МагияПревращенияВJSON()
ПримерСтруктурыJSON = Новый Структура;
ПримерСтруктурыJSON.Вставить("codе", "");
СтруктураВJSON = Неопределено;
Проверить(СтруктураВJSON = Неопределено, Ложь);
КонецФукнции
Заставляем тест работать
Функция Тест_МагияПревращенияВJSON()
ПримерСтруктурыJSON = Новый Структура;
ПримерСтруктурыJSON.Вставить("codе", "");
СтруктураВJSON = МагияПревращенияВJSON(Неопределено);
JsonНеСформирован = (СтруктураВJSON = Неопределено);
Проверить(JsonНеСформирован, Ложь);
КонецФункции
Хорошо бы знать, какой тест у нас прошёл, а какой нет, добавляем сообщение
Функция ВыполнитьТесты() Экспорт
мСообщить("Запущен тест: Тест_СтруктураНоменклатурыДляВыгрузки");
Тест_СтруктураНоменклатурыДляВыгрузки();
мСообщить("Запущен тест: Тест_МагияПревращенияВJSON");
Тест_МагияПревращенияВJSON();
КонецФункции
у нас появилось дублирование сообщений, но уберём это уже на след. итерации
Шаг 3 - Пишем тест "Тест работоспособности запроса данных из БД"
Пишем тест, который закончится ошибкой
Функция ПолучитьДанныеНоменклатурыДляВыгрузки(СсылкаНаНоменклатуру)
возврат Неопределено;
КонецФункции
Функция Тест_ПолучитьДанныеНоменклатурыДляВыгрузки()
СсылкаНаНоменклатуру = Справочники.Номенклатура.ЭталоннаяНоменклатура;
Выборка = ПолучитьДанныеНоменклатурыДляВыгрузки(СсылкаНаНоменклатуру);
Проверить(Выборка.Следующий(), Истина);
КонецФункции
Заставляем тест работать
Кстати, при постоянном запуске тестов тут сразу и поймаем ошибку с неверным параметром, ибо тест будет завершаться ошибкой
Функция ПолучитьДанныеНоменклатурыДляВыгрузки(СсылкаНаНоменклатуру)
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| Номенклатура.Код КАК Код
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Ссылка = &Ссылка";
Запрос.УстановитьПараметр("Ссылка", СсылкаНаНоменклатуру);
возврат Запрос.Выполнить();
КонецФункции
Функция Тест_ПолучитьДанныеНоменклатурыДляВыгрузки()
СсылкаНаНоменклатуру = Справочники.Номенклатура.ЭталоннаяНоменклатура;
Результат = ПолучитьДанныеНоменклатурыДляВыгрузки(СсылкаНаНоменклатуру);
Проверить(Результат.Пустой(), Ложь);
КонецФункции
Проводим рефакторинг, убирая кучу сообщений
Функция ВыполнитьТесты() Экспорт
МассивТестов = Новый Массив;
МассивТестов.Добавить("Тест_СтруктураНоменклатурыДляВыгрузки");
МассивТестов.Добавить("Тест_МагияПревращенияВJSON");
МассивТестов.Добавить("Тест_ПолучитьДанныеНоменклатурыДляВыгрузки");
Для каждого Тест из МассивТестов Цикл
Сообщить(СтрШаблон("Запущен тест: %1", Тест));
Попытка
КомандаВыполнения = СтрШаблон("%1()", Тест);
Выполнить(КомандаВыполнения);
Исключение
Проверить(Истина, Ложь); // просто чтобы вывелость типовое "Тест провален"
КонецПопытки;
КонецЦикла;
КонецФункции
Возможно, на каких-то этапах код получается не самым красивым и оптимальным - это нормально! Сначала пишем тест, потом заставляем тест работать и только потом занимаемся рефакторингом
Шаг 4 - Пишем тест "Тест работоспособности заполнения данных структуры"
Пишем тест, который закончится ошибкой
Функция Тест_ЗаполнитьСтруктуруВыгрузкиНоменклатуры()
Запрос = Новый Запрос;
Запрос.Текст = "Выбрать 1 как code";
Выборка = Запрос.Выполнить().Выбрать();
ДанныеJSON = Неопределено;
Проверить(ДанныеJSON = Неопределено, Ложь);
КонецФункции
Заставляем тест работать
Функция ЗаполнитьСтруктуруВыгрузкиНоменклатуры(Выборка)
Если Выборка.Следующий() Тогда
ДанныеJSON = СтруктураНоменклатурыДляВыгрузки();
ЗаполнитьЗначенияСвойств(ДанныеJSON, Выборка);
Иначе
ДанныеJSON = Неопределено;
КонецЕсли;
возврат ДанныеJSON;
КонецФункции
Функция Тест_ЗаполнитьСтруктуруВыгрузкиНоменклатуры()
Запрос = Новый Запрос;
Запрос.Текст = "Выбрать 1 как code";
Выборка = Запрос.Выполнить().Выбрать();
ДанныеJSON = ЗаполнитьСтруктуруВыгрузкиНоменклатуры(Выборка);
Если ДанныеJSON = Неопределено Тогда
РезультатТеста = Ложь;
Иначе
РезультатТеста = (ДанныеJSON.code = 1);
КонецЕсли;
Проверить(РезультатТеста, Истина);
КонецФункции
Функция СтруктураНоменклатурыДляВыгрузки()
данныеJSON = Новый Структура;
данныеJSON.Вставить("code", "");
Возврат данныеJSON;
КонецФункции
Функция Тест_СтруктураНоменклатурыДляВыгрузки()
ЭталонныеДанныеJSON = Новый Структура;
ЭталонныеДанныеJSON.Вставить("codе", "");
КлючиРавны = СравнитьСтруктуры(ЭталонныеДанныеJSON, СтруктураНоменклатурыДляВыгрузки());
Проверить(КлючиРавны, Истина);
КонецФункции
Функция Тест_МагияПревращенияВJSON()
ПримерСтруктурыJSON = Новый Структура;
ПримерСтруктурыJSON.Вставить("codе", "");
СтруктураВJSON = МагияПревращенияВJSON(ПримерСтруктурыJSON);
JsonНеСформирован = (СтруктураВJSON = Неопределено);
Проверить(JsonНеСформирован, Ложь);
КонецФункции
Функция ПолучитьДанныеНоменклатурыДляВыгрузки(СсылкаНаНоменклатуру)
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| Номенклатура.Код КАК Код
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Ссылка = &Ссылка";
Запрос.УстановитьПараметр("Ссылка", СсылкаНаНоменклатуру);
возврат Запрос.Выполнить();
КонецФункции
Функция Тест_ПолучитьДанныеНоменклатурыДляВыгрузки()
СсылкаНаНоменклатуру = Справочники.Номенклатура.ЭталоннаяНоменклатура;
Результат = ПолучитьДанныеНоменклатурыДляВыгрузки(СсылкаНаНоменклатуру);
Проверить(Результат.Пустой(), Ложь);
КонецФункции
Функция ЗаполнитьСтруктуруВыгрузкиНоменклатуры(Выборка)
Если Выборка.Следующий() Тогда
ДанныеJSON = СтруктураНоменклатурыДляВыгрузки();
ЗаполнитьЗначенияСвойств(ДанныеJSON, Выборка);
Иначе
ДанныеJSON = Неопределено;
КонецЕсли;
возврат ДанныеJSON;
КонецФункции
Функция Тест_ЗаполнитьСтруктуруВыгрузкиНоменклатуры()
Запрос = Новый Запрос;
Запрос.Текст = "Выбрать 1 как code";
Выборка = Запрос.Выполнить().Выбрать();
ДанныеJSON = ЗаполнитьСтруктуруВыгрузкиНоменклатуры(Выборка);
Если ДанныеJSON = Неопределено Тогда
РезультатТеста = Ложь;
Иначе
РезультатТеста = (ДанныеJSON.code = 1);
КонецЕсли;
Проверить(РезультатТеста, Истина);
КонецФункции
Функция ВыполнитьТесты() Экспорт
МассивТестов = Новый Массив;
МассивТестов.Добавить("Тест_СтруктураНоменклатурыДляВыгрузки");
МассивТестов.Добавить("Тест_МагияПревращенияВJSON");
МассивТестов.Добавить("Тест_ПолучитьДанныеНоменклатурыДляВыгрузки");
МассивТестов.Добавить("Тест_ЗаполнитьСтруктуруВыгрузкиНоменклатуры");
Для каждого Тест из МассивТестов Цикл
Сообщить(СтрШаблон("Запущен тест: %1", Тест));
Попытка
КомандаВыполнения = СтрШаблон("%1()", Тест);
Выполнить(КомандаВыполнения);
Исключение
мСообщить(ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
Проверить(Истина, Ложь); // просто чтобы вывелость типовое "Тест провален"
КонецПопытки;
КонецЦикла;
КонецФункции
Функция Проверить(Значение1, Значение2)
Если Значение1 = Значение2 Тогда
Результат = "[V] Тест пройден";
Иначе
Результат = "[X] Тест провален";
КонецЕсли;
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = Результат;
Сообщение.Сообщить();
КонецФункции
// заглушка, для демонстрации её реализация необязательна
Функция СравнитьСтруктуры(Структура1, Структура2)
Возврат Структура2 <> Неопределено;
КонецФункции
Функция МагияПревращенияВJSON(ЭталонныеДанныеJSON)
Если ЭталонныеДанныеJSON = Неопределено Тогда
Возврат Неопределено;
Иначе
Возврат "JSON";
КонецЕсли;
КонецФункции
// обёртка, чтоб не использовать устаревший метод
Функция мСообщить(ТекстСообщения)
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = ТекстСообщения;
Сообщение.Сообщить();
КонецФункции
Польза от подобного подхода
1. Получилось, что запрос к БД, отделён от логики обработки запроса, а логика заполнения JSON, от логики отправки, другими словами код стал слабосвязанным
2. Алгоритм разбился на атомарные функции, выполняющее ровно 1 действие
3. Начинает формироваться понимание о том, а что же такое тестирование, что такое размер теста (шаг теста)
4. Если потребуется изменить выгрузку, добавив поля или изменив запрос - вы будете спокойны, т.к. ключевые точки алгоритма уже закрыты тестами и перепроверить выгрузку не составит большого труда
Заключение
Как видите, начинать свой путь в тестирование, в частности в unit-тестирование достаточно легко. Просто пишите код. Не обязательно в начале пути использовать доп. фреймворки и чужие разработки.
Основные преимущества подобного подхода:
- проделав это несколько раз руками - в голове начнётся складываться понимание, что это, зачем это и действительно ли это вам нужно
- если сразу взяться за чужой фреймворк - может запросто упасть мотивация при первой же возникшей проблеме или же при виде количества инструментов, их возможностей и вариаций. А тут такого произойти не может, ведь всё это пишете вы сами
- идеолог движения Кент Бек также советует начинать ознакомление с попытки разработки своего фреймворка
Для тех, кто захочет копать дальше - полезные отсылки:
- подход, показанный в статье называется "test-driven development" или "разработка через тестирование".
- Одним из идеологов данного подхода является Кент Бек, который написал книгу "Экстремальное программирование: разработка через тестирование" - очень советую к ознакомлению
- и вообще читайте первоисточники, так картина знаний в голове будет более цельной.
- После первичного погружения в тему, вам возможно не захочется возиться со своим фреймворком дальше, а взять уже готовый. Пока я могу отослать вас к четырём различным инструментам:
И напоследок личное мнение. Я не советую начинать с BDD, ибо этот подход имеет гораздо более высокий порог вхождения и может поначалу тяжело заходить в голову, что, в свою очередь, может запросто лишить мотивации к дальнейшему погружению в тему. К нему стоит обратиться уже после того, как вы освоились в unit-тестировании, упёрлись в его границы и хотите большего. Или когда внедряете полноценное тестирование продукта в команду. Но это тема отдельной статьи.
На этом у меня всё, большое вам спасибо за прочтение.