DUnitX is an open-source unit test framework based on the NUnit test framework, including some ideas from xUnit as well. The RAD Studio integration of DUnitX framework enables you to develop and execute tests against Win32, Win 64, macOS, and Linux in Delphi applications.
The DUnitX testing framework provides its own set of methods for testing conditions. You can use the provided methods to test a large number of conditions. These methods represent common assertions although you can also create your own custom assertions.
DUnitX uses Generics and Anonymous methods. DUnitX uses attributes under Delphi personality.
Contents
Developing Delphi DUnitX Tests
The following sample Delphi program defines two functions that perform a simple addition and subtraction:
DUnitX Functions
DUnitX provides Assert class with a number of functions that you can use in your tests.
The table shows some of these functions.
Function
Description
Checks that a routine works.
Checks that a routine fails.
Checks to see if items are equal.
Checks to see if items are not equal.
Checks to see that two items have the same value.
Checks to see that two items do not have the same value.
Checks to see if the item is in a list.
Checks to see if the item is not in a list.
Checks that a condition is true.
Checks that a condition is false.
Checks to see if the value of an item is empty.
Checks to see if the value of an item is not empty.
Checks to see that an item is null.
Checks to see that an item is not null.
Checks to see if the method will raise an exception.
Checks if a string starts with a specified substring.
Checks if a class is descendant of a specified class.
Checks if the item matches with a specified pattern.
DUnitX Test Runners
A test runner allows you to run your tests independently of your application. In a DUnitX test project, the test runner code from the DUnitX framework is compiled directly into the generated executable making the test project itself a test runner. The integrated DUnitX framework provides two test runners:
Многие разработчики говорят о юнит-тестах, но не всегда понятно, что они имеют в виду. Иногда неясно, чем они отличаются от других видов тестов, а порой совершенно непонятно их назначение.
Доказательство корректности кода
Автоматические тесты дают уверенность, что ваша программа работает как задумано. Такие тесты можно запускать многократно. Успешное выполнение тестов покажет разработчику, что его изменения не сломали ничего, что ломать не планировалось.
Провалившийся тест позволит обнаружить, что в коде сделаны изменения, которые меняют или ломают его поведение. Исследование ошибки, которую выдает провалившийся тест, и сравнение ожидаемого результата с полученным даст возможность понять, где возникла ошибка, будь она в коде или в требованиях.
Отличие от других видов тестов
Все вышесказанное справедливо для любых тестов. Там даже не упомянуты юнит-тесты как таковые. Итак, в чем же их отличие?
Ответ кроется в названии: «юнит» означает, что мы тестируем не всю систему в целом, а небольшие ее части. Мы проводим тестирование с высокой гранулярностью.
Это основное отличие юнит-тестов от системных, когда тестированию подвергается вся система или подсистема, и от интеграционных, которые проверяют взаимодействие между модулями.
Основное преимущество независимого тестирования маленького участка кода состоит в том, что если тест провалится, ошибку будет легко обнаружить и исправить.
И все-таки, что такое юнит?
Часто встречается мнение, что юнит — это класс. Однако это не всегда верно. Например, в C++, где классы не обязательны.
«Юнит» можно определить как маленький, связный участок кода. Это вполне согласуется с основным принципом разработки и часто юнит — это некий класс. Но это также может быть набор функций или несколько маленьких классов, если весь функционал невозможно разместить в одном.
Юнит — это маленький самодостаточный участок кода, реализующий определенное поведение, который часто (но не всегда) является классом.
Это значит, что если вы жестко запрограммируете зависимости от других классов в тестируемый, ошибку, которая вызвала падение теста, будет сложно локализовать, и высокая гранулярность, которая необходима для юнит-тестов, будет потеряна.
Отсутствие сцепления необходимо для написания юнит-тестов.
Другие применения юнит-тестов
Кроме доказательства корректности, у юнит-тестов есть еще несколько применений.
Тесты как документация
Юнит-тесты могут служить в качестве документации к коду. Грамотный набор тестов, который покрывает возможные способы использования, ограничения и потенциальные ошибки, ничуть не хуже специально написанных примеров, и, кроме того, его можно скомпилировать и убедиться в корректности реализации.
Я думаю, что если тесты легко использовать (а их должно быть легко использовать), то другой документации (к примеру, комментариев doxygen) не требуется.
Тем не менее, в этом обсуждении после поста про комментарии видно, что не все разделяют мое мнение на этот счет.
Разработка через тестирование
При разработке через тестирование (test-driven development, TDD) вы сначала пишете тесты, которые проверяют поведение вашего кода. При запуске они, конечно, провалятся (или даже не скомпилируются), поэтому ваша задача — написать код, который проходит эти тесты.
Основа философии разработки через тестирование — вы пишете только тот код, который нужен для прохождения тестов, ничего лишнего. Когда все тесты проходят, код нужно отрефакторить и почистить, а затем приступить к следующему участку.
Об этой технике разработки можно много дискутировать, но ее неоспоримое преимущество в том, что TDD не бывает без тестов, а значит она предостерегает нас от написания жестко сцепленного кода.
И, поскольку TDD предполагает, что нет участков кода, не покрытых тестами, все поведение написанного кода будет документировано.
Возможность лучше разобраться в коде
Когда вы разбираетесь в плохо документированном сложном старом коде, попробуйте написать для него тесты. Это может быть непросто, но достаточно полезно, так как:
Бывает, кодишь 10 минут, а дебажишь 2 часа. Чтобы такого не случилось, пилите юнит-тесты. Михаил Фесенко рассказал, как их правильно готовить.
Фесенко Михаил, можно просто Фес. Разработчик, раньше работал системным администратором, пишет на чём скажут, но пока писал на PHP, Go, Python, Bash. Сейчас работает в «Яндекс.Облаке», до этого работал во «ВКонтакте». Любит жену, кино и снимать видео =)
Юнит-тест (unit test), или модульный тест, — это программа, которая проверяет работу небольшой части кода. Разработчики регулярно обновляют сайты и приложения, добавляют фичи, рефакторят код и вносят правки, а затем проверяют, как всё работает.
Тестировать систему целиком после каждого обновления — довольно муторно и неэффективно. Поэтому обновлённые или исправленные части кода прогоняют через юнит-тесты.
Особенности юнит-тестов
На практике используют разные тесты — их разделяют по уровню абстракции с помощью пирамиды Майка Кона :
Чем выше тест в пирамиде, тем больше частей программы он затрагивает. Высокоуровневые тесты «ближе к бизнесу»: они проверяют бизнес-логику и пользовательские процессы. А те, что внизу пирамиды, помогают найти проблемы в отдельных частях кода. Например, какую-нибудь функцию, которая генерирует имя файла.
В отличие от них, юнит-тесты нужны в следующих случаях:
Некоторые программисты пишут только юнит-тесты, а на интеграционные или E2E-тесты жалеют времени. На самом деле нужно покрывать систему всеми видами тестов, чтобы знать, как взаимодействуют друг с другом разные части программы, какие промежуточные результаты они выдают. Но в то же время, если юнит-тесты показывают ошибку, её покажет и интеграционный, и E2E-тест.
Процесс юнит-тестирования
Для юнит-тестирования подключают тестовые фреймворки — они позволяют «мокать», то есть имитировать функции. В коде больших проектов много зависимостей: одна функция вызывает другую и влияет на разные части программы. Но, как правило, достаточно проверить функции «в вакууме», отдельно от остального кода. Для этого и нужен тестовый фреймворк — он моделирует условия, в которых функция А вызывает функцию Б изолированно от других функций.
Простой пример: у нас есть функция на Go, которая получает id бэкапа и возвращает имя бэкап-файла:
Протестируем её с помощью набора входных и выходных данных. Они должны учитывать все ситуации, поэтому не забываем про негативные кейсы — когда программа возвращает ошибку. Вот набор тестовых данных:
В первую очередь я прописал запрещённые данные (-1 и 0) и слишком большое значение (10200300). Когда пользователь их вводит, функция не должна возвращать результат. Вместо этого мы ждём сообщения об ошибке: BAD_ID или BACKUP_ID_TOO_BIG. Когда же функция получает валидный id, она выводит отформатированное имя файла, например Backup#000010.
А вот и код самого теста:
Порой код для тестирования даже больше основного — и это норма. Но иногда всё-таки стоит задуматься, на самом ли деле тест должен быть таким объёмным. Я бы посоветовал покрывать тестами только те фрагменты кода, которые вы планируете менять. Или сложные части, которые, скорее всего, придётся чинить или поддерживать.
Некоторые разработчики мокают всё подряд. Из-за этого тесты становятся хрупкими, а код — сложным и непонятным. На самом деле для юнит-тестирования достаточно лишь немного переписать код, а огромные функции лучше разбить на более мелкие.
В старой хорошей книге «Экстремальное программирование» есть классная мысль: сначала пишите тест, а только потом программу. Это клёвый подход, но не все могут так делать (а кто-то просто не хочет тратить время).
Как покрыть код юнит-тестами
Есть разработчики, которые не проводят модульное тестирование: «Ой, у нас большой проект, и переписать 1000 строк под тесты или замокать их — слишком запарно». На самом деле покрыть код тестами несложно. Вот несколько советов.
Написали код — напишите тест. Я видел много проектов, в которых юнит-тесты писали по принципу «новый код — новый тест». Думаю, это правильный подход, ведь, когда добавляешь в программу что-то новое, она часто ломается. К тому же, если писать тесты сразу, не придётся переворачивать весь код, когда он разрастётся.
Есть более жёсткий принцип: новый код без тестов на ревью не принимается. Конечно, он работает, если сроки не горят, — иначе программист рефакторит или покрывает его тестами позже.
Используйте тестовый фреймворк. В тестировании не нужно изобретать велосипед. Для популярных языков уже есть готовые решения, поэтому достаточно вбить в поиске test frameworks, и вы получите целый список. Вот, например, результат для Python:
Пишите простые тесты. Надо понимать, что происходит с входными данными и какой результат должна вернуть функция. Если непонятно — меняем нейминг и разбиваем функции на более мелкие, избавляемся от зависимостей. Пусть одна функция принимает результат, а другая возвращает. Так проще тестировать.
Допустим, у нас есть такая функция:
Её не нужно прогонять через юнит-тест, потому что тогда придётся мокать process_a, process_b и prepare_output. Тут нужен интеграционный тест, который проверит, как эти компоненты взаимодействуют между собой. Вообще, если код сложно покрывать юнит-тестами, используйте интеграционные — они проверяют общую работу системы, модуля или библиотеки.
Не забывайте про негативные тесты. Это the best practice. Что произойдёт, если передать в программу неправильные данные? Какую ошибку она выведет и выведет ли?
Покрывайте тестами все циклы и if-else. Этот совет касается кода, который нужно поддерживать. Если ему не следовать, на одной из итераций правок вы или ваш коллега просто всё сломаете.
Проверяйте качество тестов. Сделать это поможет мутационное тестирование. Мутационный фреймворк случайно меняет константы и значения в условных операторах и циклах, создаёт копию кода, в которой поочерёдно меняет условия. Например, было >= или было COUNT=3, а стало COUNT=10. Каждая замена тестируется: если код поменялся, а тесты не упали, значит, код не покрыт тестами.
На мутационное тестирование уходит много времени. Можно подключить плагин, который считает code coverage по тесту и выдаёт отчёт. Например, у нас покрыто тестами 43 тысячи строк кода, а 10 тысяч — нет. Значит, code coverage 81%. Но тут важен не только сам процент, но и качество — какие именно фрагменты кода и какими именно тестами покрыты. Например, не всё может быть под юнит-тестами — часть может перекрываться интеграционными.
Обеспечьте достаточный процент покрытия кода. Года три-четыре назад я был фанатиком стопроцентного покрытия. Конечно, безумно круто, когда ты всегда знаешь, что именно сломалось. Но в продакшне этого добиться сложно — да и не нужно. Исключение — маленькие проекты или «жёсткие» команды, для которых полное покрытие в приоритете.
На самом деле, code coverage в 70–90% — уже крутой показатель, но и меньше 70% — тоже плохо. И ещё важный момент: новый код не должен понижать уровень code coverage.
Проверить code coverage можно с помощью coveralls.io:
Coveralls принимает результаты тестов и выдаёт отчёт: показывает процент покрытия и как он изменился с последнего теста.
Не делайте хрупкие тесты. Если тест нестабильный и регулярно падает, его называют хрупким. Его результат может зависеть от дня недели, времени суток, чётности или нечётности запуска. Бывает, две функции работают параллельно и на итоговый результат влияет то, какая из них закончит выполняться первой. Такие функции лучше разбивать на несколько простых и тестировать по отдельности. Мокайте всё что нужно, чтобы сделать тест управляемым, но не переборщите — иначе код будет сложно поддерживать.
Допустим, мы написали юнит-тесты для двух функций. Но не учли, что первая функция сохраняет данные в глобалке, а вторая из-за этого меняет своё поведение. В результате первый тест проходит нормально, а второй падает или ведёт себя странно. А всё потому, что мы не сбросили состояние глобальной переменной.
Следите за скоростью тестов. Тесты должны работать быстро. Если они проверяют кусок кода 10–15 минут — разработчики устанут ждать и отключат их нафиг. Поэтому регулярно проверяйте скорость, ищите узкие места и оптимизируйте тесты. Если есть проблемы, подключитесь через дебаггер — возможно, основной код плохо оптимизирован и искать проблему нужно в продакшне.
Преимущества юнит-тестов
Если у вас ещё остались сомнения, писать юнит-тесты или нет, вот несколько аргументов за. Итак, чем полезны юнит-тесты.
Упрощают работу — находят ошибки, которые вы можете не заметить (меня это много раз спасало). Например, меняешь одну строчку, чтобы поправить логи, а ломается весь код. Благодаря тестам я узнавал об этом ещё до продакшна.
Понятно документируют код. Если вам неочевидно, как работает та или иная функция, можно пройти дальше по коду или открыть юнит-тест. По нему сразу видно, какие параметры принимает функция и что отдаёт после выполнения. Это упрощает жизнь тем, кто работает с чужим кодом.
Помогают ничего не сломать при рефакторинге. Бывает, что код написан непонятно и ты не можешь его отрефакторить, потому что наверняка что-то сломаешь в продакшне. А с тестами код можно смело рефакторить.
Упрощают разработку. Кажется, что юнит-тесты всё усложняют, ведь нужно написать в два раз больше кода — не только функцию, но и тест к ней. Но я много раз убеждался: когда пишешь код без тестов, потом тратишь гораздо больше времени на поиск и исправление ошибок.
Бывает, бац-бац — и в продакшн, а потом понеслось: исправляешь код первый, второй, третий раз. И постоянно вспоминаешь, как тестировать его вручную. У меня даже были файлики с входными данными для таких проверок. Тогда я тестировал программы вручную, по бумажке, и тратил на это уйму времени. А если бы написал юнит-тест, нашёл бы эти баги сразу и не переписывал код по несколько раз.
В коммерческой разработке без юнит-тестов никуда
Сейчас в коммерческой разработке без тестов почти не работают — а в большинстве компаний от разработчиков даже требуют покрывать код юнит-тестами. Везде, где я работал в последние несколько лет, тоже было такое правило. Ведь если в команде кто-то факапит, то может развалиться вся работа — а тестирование как раз защищает от краха.
Современные компании подписывают SLA — гарантируют работоспособность сервиса. Если продукт упадёт, бизнесу придётся заплатить деньги. Поэтому лучше подождать тестов и не катить код, который положит весь продакшн. Даже если сайт или приложение пролежат всего две минуты, это ударит по репутации и дорого обойдётся компании.
Чтобы лучше понять юнит-тесты, изучите тестовые фреймворки вашего языка. А потом найдите крупные open-source-проекты, которые их используют, и посмотрите, как они работают. Можно даже скачать проект и поиграть с тестами, чтобы глубже погрузиться в тему.
Чтобы познать тонкости разработки и тестирования приложений, лучше сразу учиться у практикующих профессионалов. Приходите в университет Skillbox, выбирайте курс и осваивайте программирование под присмотром экспертов.
Основная идея юнит (или модульного, как его еще называют) тестирования – тестирование отдельных компонентов программы, т.е. классов и их методов. Разрабатывать код, покрытый тестами, весьма полезно, потому что при их правильном использовании практически исключается возможность регресии в истории развитии программы – «что-то новое добавили, половина старого слегла». Также сейчас весьма модна методология разработки “TDD” — Test Driven Development. Согласно ей, программист вначале разрабатывает набор тестов для будущей функциональности, просчитывает все варианты выполнения, и лишь потом начинает писать непосредственно рабочий код, подходящий под уже написанные тесты.
Так как существование тестов в программе является не только подтверждением квалификации разработчика, но и зачастую требованием заказчика, я решил заняться этим вопросом и «пощупать» тесты вблизи.
Работаю я в основном в Visual Studio, пишу на шарпе, а значит выбор был почти ограничен двумя продуктами – Nunit и Unit Testing Framework.
Unit Testing Framework — это встроенная в Visual Studio система тестирования, разрабатываемая Майкрософт, постоянно развивающаяся( в числе последних обновлений – возможность тестирования UI, о чем уже писали на хабре), и что немаловажно, она почти наверняка будет существовать все время, пока есть Visual Studio, чего не скажешь о стронних разработках. Отличная интеграция в IDE и функция подсчета процента покрытия кода в программе окончательно склонили чашу весов – выбор был сделан. В сети присутствует немаленькое количество разнообразных туториалов по тестированию, но все они обычно сводятся к тестированию самописного калькулятора или сравнению строк. Это вещи, конечно, тоже необходимые и важные, но на серьезные примеры они тянут плохо, если сказать откровенно – совсем не тянут. Такие задачи я и сам могу протетстировать даже в уме.
Вот список более серьезных задач • проверка корректности создания БД • проверка корректности работы бизнес-логики • получение пользы от всего этого(в моем случае польза была получена))
Тестирование БД
Unit Testing Framework позволяет тестировать разнообразные аспекты работы БД – проверка схемы, количества записей в таблицах, хранимых процедур, времени выполнения запросов, их результатов и многое другое.
Начинаем тесты
Создаем новое решение типа TestProject. Назовем его LinkCatalog.Tests. И добавляем в него новый тест Database Unit Test Появляется окно настройки соединения с БД. Настраиваем соединение с нашей БД и жмем ОК. В этом же окне можно указать параметры автогенерации данных для тестов. Эта функция использует Data Generation Plan и позволяет заполнить таблицу базы тестовыми значениями, используя шаблоны и даже регулярные выражения. Нажимаем ОК и попадаем на окно тестирования БД.
Тест №1
Теперь устанавливаем условие корректности теста. Как я говорил, существует большой список всевозможных критериев, но нас интересует один из самых простых – ScalarValue. Все опции условия настраивается в окошке Properties.
Все! На этом первый тест закончен. Запускаем и смотрим Что и требовалось доказать – строки успешно хранятся в базе.
Тест №2
Теперь настало время заняться уже более реальной проверкой, чем количества записей. Речь идет о хранимой процедуре. А вдруг разработчики базы допустили в ней критическую ошибку? А ведь это важнейшая часть работы подсистемы аутентификации пользователей! Создаем новый тест БД, нажав на зеленый плюсик
SELECT * FROM Users WHERE >
Здесь уже используется другое условие – ожидается, что в выборке будет ровно 1 строка.
Этот тест также завершается успешно, что не может не радовать. По результатам тестирования базы данных можно сказать, что она работает стабильно и ожидаемо. Сейчас, покрытие процедур БД достигает 100% — предела, которого непросто добиться в более сложном приложении или базе с бОльшим количеством таблиц/процедур/связей.
Юнит-тесты кода
namespace LinkCatalog.DAL < public class UserModel < . public static int GetUserIdByName(string username) < string query = » SELECT ID FROM Users WHERE Login = @login;»; DB. get ().CommandParameters. Add ( new SqlParameter(«@login», username));
return id; > > public class DB < . private static DB instance;
public static DB get () < if (instance == null ) instance = new DB();
public List CommandParameters; private DB() < this. connection = new SqlConnection(WebConfigurationManager.ConnectionStrings[«DBConnectionString»].ConnectionString); this.CommandParameters = new List(); >
public object GetOneCell(string query) < SqlCommand sc = new SqlCommand(query, this. connection );
object res = sc.ExecuteScalar(); this.CommandParameters.Clear();
using System; using System.Text; using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting;
Как видно, ошибка заключается в конструкторе класса DB – он не может найти файл конфигурации и, вследствие этого, тест завершается не только неудачно, но и с ошибкой.
Решение проблемы конфигурационных файлов довольно простое: Нет, среда тестирования не подключит web.config автоматически. Вместо этого каждый тест-проект создает свой файл конфигурации app.config, и все, что требуется – дописать в него необходимые настройки.
Теперь все ОК! Вот так вот тестирование помогает выявить некоторые ошибки в программе, пускай они возникают от забывчивости/лени/не знания, но так их исправить легче, чем на живом сервере. Этот топик, естественно, не покрывает всех аспектов тестирования программ и лишь приоткрыл эту область. За кадром осталась возможность условных тестов, зависящих от других тестов, тестов пользовательских интерфейсов и другие вещи.
Тестируйте свои программы и пускай багов у Вас будет мало, а фич много-много!