В этой статье будет юнит тестированиеи его принципы; интерфейсы и абстрактные классы, что означает наследование и много ООП в Codesys; принципы написания тестируемого кода и примеры плохого кода от меня, а также EnUnitTest — библиотека тестирования.
Многие спорят: стоит ли писать тесты или нет, как их писать, сколько их писать, кто их будет писать и зачем они вообще нужны?
Плохой юнит-тест — это еще одна неработающая часть вашего кода, а в среде АСУТП при программировании ПЛК для написания юнит теста надо прям извратиться, так как тестирование должно проходить до попадания на физический ПЛК или на софт ПЛК конечной точки.
ЮНИТ ТЕСТ
Юнит тест или модульное тестирование — процесс в программировании, позволяющий проверить на корректность отдельные модули исходного кода программы.
Так зачем нужна такая страшная вещь?
Модульное тестирование позволяет проверить на работоспособность небольшие куски программы — логические модули. После чего данный тест становится единственной точкой истины и если после изменения в логическом модуле он все также проходит тест, то увеличивает шанс того, что мы ничего не сломали. К тому же в тесте мы можем проверить различные граничные условия.
Логика работы юнит теста
Для того чтобы протестировать логический модуль нам необходимо сформировать набор входных данных и соответствующий набор выходных данных.
После скармливаем нашему логическому модулю входные данные, забираем что у него появилось на выходе, и сравниваем с нашим набором выходных данных. И тут данные либо совпадают, либо нет и мы уже знаем прошел тест или нет.
Для полноценного тестирования необходимо проверить нормальные состояния, граничные состояния, а также убедиться, что модуль показывает неправильно там, где он должен показать неправильно.
Три правила модульного тестирования
Первый закон. Не пишите код продукта, пока не напишете отказной модульный тест
Второй закон. Не пишите модульный тест в объеме большем, чем необходимо для отказа Невозможность компиляции является отказом.
Третий закон. Не пишите код продукта в объеме большем, чем необходимо для прохождения текущего отказного теста
ТЕСТИРУЕМЫЙ КОД
Первоочередная задача-научиться писать тестируемый код.
Писать тестируемый код, в моем понимании, правильно делить его на функции, правильно оформлять функции и правильно организовывать работу с данными.
Под “функцией” я обобщаю как функции, так и методы.
Логика функционального блока должна быть разбита на функции. Тестирование целого функционального блока — трудоемкий процесс, который никогда не окупится, а скорее всего просто невозможен.
На изображении выше максимально не тестируемый функциональный блок так как много условий, они все отвечают за разные действия, а на вход всего у нас приходят DUT в виде структур.
Функция в функциональном блоке должна решать одну задачу.
Вот здесь уже есть такое разбиение по функциям, но здесь каждая функция решает не одну задачу.
А вот этот функциональный блок можно спокойно оттестировать. Каждая функция решает одну задачу.
Если подвести итог, то единицей тестирования у нас будет функция или метода, который будет отвечать за решение одной задачи.
EnUnitTest
Как работает один юнит тест.
Для работы библиотеки требуется создать универсальный тестовый модуль. Чтобы это все еще работало по паттерну Построение — операция — проверка.
Основа всего — интерфейс.
- BuildData — метод построения данных или инициализации нужных значений
- Operation — метод в котором будет выполняться логика тестирования
- Check — метод проверки выходных результатов.
- TEST — метод, в котором будет реализована машина состояний(конечные автоматы) всего процесса тестирования
- AssertTrue — метод, который возвращает TRUE когда на входе TRUE
- AssertFalse — метод, который возвращает TRUE когда на входе FALSE
- AsseertEquals — метод, который возвращает TRUE, когда входные значения одинаковые.
Теперь создадим абстрактный функциональный блок, который реализует данный интерфейс.
Абстрактный функциональный блок — определяет, что функциональный блок имеет отсутствующую или неполную реализацию и не может иметь экземпляра. Абстрактные FB используются исключительно как базовые функциональные блоки, и их реализация обычно происходит в производных FB.
В абстрактном блоке реализуем все Assert — они точно не будут повторяться и метод Test.
Для создания абстрактного функционального блока, который реализует интерфейс надо это все указать в мастере создания POU:
Получается структуру функционального блока для реализации.
Тут небольшая магия, так как появились свойства функционального блока. Они также описаны в интерфейсе, Но они не реализовывались кроме метода getTestName и getTestStatus.
Методы реализации проверки стандартны, можно будет посмотреть в исходниках. Но можно это все самому реализовать. Так что нам главное логика возвращаемых значений.
Методы проверки возвращают в конце следующий значения
Методы AssertEquals, AssertFalse, AssertTrue возвращают либо статус OK, который говорит, что тест пройден успешно или FAIL — тест пройден не успешно, также для сложных вариантов проверок можно выдавать ERROR — ошибка теста.
Далее реализуем метод Test. Под капотом это просто машина состояний.
Для работы этой машины нам надо немного инкапсулированных данных.
Входные данные мы будем объявлять при создании объекта функционального блока. А к переменным VAR не должно быть свободного прямого доступа. Как раз для такого я и закладывал в интерфейсе
Во время стадии “ожидание” мы инициализируем все значения на дефолт и переходим далее.
“Построение тестового объекта” — тут происходит инициализация нужными значения для тестовый функции тестового объекта..
“Выполнение функции” — вызов тестовой функции, которая будет оперировать подготовленными данными из предыдущей стадии.
“Проверка результата” — проверяем результаты выполнения функции с эталоном.
“Завершение теста” — прокидываем статус теста из вариантов представленных выше. И выходим из теста
100 — ошибка. Обработка ошибки и выбрасываем сигнализацию о ней.
Как работает автоматический запуск серии тестов — Tester.
Следующий шаг — запуск серии юнит тестов и логирование выполнения результата. В конечном итоге требуется минимизировать ручного кода для тестирования.
Интерфейс
Четыре метода
End — Завершение всей серии тестирования из-за отсутствия юнит тестов
Next — Переход к следующему тесту
Start — Запуск теста
Write — Запись результата теста.
Раз есть интерфейс, то давайте его реализуем. Опять в абстрактном функциональном блоке.
Почему абстрактные функциональные блоки?
Потому что при наследовании от абстрактного функционального блока вы можете изменить реализацию любого метода под ваши нужды, но если какой-то другой функциональный блок принимает на вход родительский абстрактный функциональный блок, ссылку на родительский функциональный блок или экземпляр реализованного интерфейса, то и ваш функциональный блок туда подойдет.
После создания набиваем внутренностями
Здесь нас интересует интерфейс iWriter, поля UnitTest и UnitTestInfo.
Начнем с информации. udtTest — это простая структура результата тестирования
Тут мы пишем имя серии теста, имя конкретного теста и его результат.
Поле UnitTest — является указателем на абстрактный функциональный блок из прошлой главы.
А указатель формируется из полей pUnitTest, uiUnitTestArraySize, uiUnitTestSize.
Такая математика нужна из-за того, что конечный функциональный блок может отличаться по размеру от абстрактного функционального блока.
iWriter — еще один интерфейс, реализация которого полностью ложиться на конечного пользователя.
Возвращаемся к абстрактному функциональному блоку. Самое интересное — это тело функционального блока.
Да. Тут мы основной функционал по старой доброй традиции вынесли в тело функционального блока и это тоже конечный автоматы.
Вот список всех основных стадий. Далее представлю код.

iWriter
Последний интерфейс с двумя методами один из них FB_Init
А так как в моей реализации мало строчек, то я ее приведу прям тут
Данная реализация просто записывает в GVL результаты тестирования.
ПРИМЕР ИСПОЛЬЗОВАНИЯ
Теперь немного практического применения. Я буду тестировать максимально сложную функцию.
Для того чтобы протестировать функцию нам надо для нее написать Unit тест. Чтобы написать Unit Test для конкретной функции нам требуется создать Функциональный блок, который будет наследоваться от абстрактного функционального блока.
Далее, те методы, которые не надо реализовывать(а это все Assert..), удаляются из структуры функционального блока, а оставшиеся три требуют реализации.
Объявляем необходимые переменные
Реализовываем метод построения данных
Реализуем метод вызова тестируемой функции
Реализуем метод проверки результатов
Вот реализация нашего теста, который будет проверять нашу функцию.
И составляем наш список юнит тестов в какой-нибудь удобной структуре.
С этим моментом разобрались. Теперь нам нужен функциональный блок, который будет перебирать наши тесты. Для этого наследуемся от нашего абстрактного функционального блока aTester.
Удаляем те методы чью реализацию не будем переписывать, чтоб воспользоваться стандартной(удаляем все методы)
И тут внимание
В теле функционального блока прописываем следующее
SUPER^() — это метод, который вызывает родительскую реализацию тела функционального блока.
Следующий момент — это логи.
Теперь надо сотворить экземпляры логов и серии тестов
И скормить для тестирования
Грузим и смотрим.
До начала тестирования проверяю логи
Они чисты.
Напоминаю, что мы тестируем функцию, которая складывает два целочисленных числа. В моем случае 2+2
В одном случае сравниваю результат с 4, во втором с 8 и хочу получить что-то из списка статусов.
Статусы 100 и 101.
Запуск тестирования происходит путем перевода входа EN в значение TRUE
И вот результат тестирования.
При сценарии 2+2 =4 получаем 100(OK), а во втором случае 2+2=8 101(FAIL)
ИТОГ
В итоге у нас получился целый фреймворк для Unit тестирования с использованием среды Codesys 3.5 и softPLС.
Также по работе можно понять трудозатраты на реализацию. Ну и ссылки на все материалы
- Плейлист с парочкой стримов, где это все реализовывалось и обтачивалось напильником
- Сама библиотека и архив с проектом, который в статье.
Если есть замечания и предложения, если нужен новый функционал в библиотеку, то можете писать на почту или оставлять issue на github.
Почта для связи: info@engcore.ru — ваши вопросы, предложения, идеи для новых статей
Телеграм канал: вот по этой ссылке — там есть новости из мира OT и какие-то заметки на скорую руку.
