#тестирование
В своей практической деятельности я часто встречаюсь с недооценкой программного тестирования разрабатываемых систем. По большей части эта недооценка формируется за счёт "отсутствия времени" или "сменой фокуса", т.к. разработчик вынужден быстрее закрыть задачу, а на возможные косяки, которые не были обнаружены сейчас, можно смело "забить". Только вот эти косяки могут всплыть в самый не подходящий и неожиданный момент.
Мне очень часто приходилось исправлять уже существующие функции, которые просто не тестировались (вручную, разумеется) на тех граничных случаях, на которых выдают неправильный результат и из раза в раз приходилось осуществлять правки, а затем снова и снова проверять всё ли там работает. Такой метод тестирования действительно способен вымотать, да и с учётом развития современных методов и подходов к тестированию это объективно не нужно.
Рассмотрим наиболее быстрый и оптимальный способ подключения фреймворка для тестирования GTest к уже существующему C++ проекту на CMake.
Предположим, файловая структура вашего проекта выглядит следующим образом:
В файле sum.cpp будет определена следующая простая функция вычисления суммы:
А в заголовке sum.h будет расположена её сигнатура:
Чтобы добавить фреймворк GTest, сначала создадим новую директорию tests и добавим в неё отдельный CMakeTests.txt файл, который будет конфигурировать модуль тестирования:
Помимо прочего, следует сразу же в директорию tests добавить файл unit\easy-unit.cpp, в котором можно сразу же описать первый простой Unit-тест:
Файловая структура проекта должна быть следующей:
С базовой настройкой тестирования почти закончили. Осталось осуществить самый важный шаг - интегрировать модуль тестов в основной CMakeLists.txt файл. Делается это довольно просто:
Статическая библиотека необходима, чтобы в тестовом проекте (который является ответвлением от текущего) нам не приходилось импортировать все зависимости нашего приложения. Иначе каждый раз придётся вручную добавлять большое множество файлов в проект тестирования через инструкцию add_executable.
И да, тестовый проект это отдельный exe-файл, и собирается он отдельно от основного проекта (это стоит учитывать при инфраструктурных задачах).
В результате мы получаем простую интеграцию системы тестирования в проект и можем добавлять различные unit и интеграционные тесты для всевозможной проверки работоспособности нашей системы. Меньше времени на не нужную рутину, больше на программирование и решение задач.
В своей практической деятельности я часто встречаюсь с недооценкой программного тестирования разрабатываемых систем. По большей части эта недооценка формируется за счёт "отсутствия времени" или "сменой фокуса", т.к. разработчик вынужден быстрее закрыть задачу, а на возможные косяки, которые не были обнаружены сейчас, можно смело "забить". Только вот эти косяки могут всплыть в самый не подходящий и неожиданный момент.
Мне очень часто приходилось исправлять уже существующие функции, которые просто не тестировались (вручную, разумеется) на тех граничных случаях, на которых выдают неправильный результат и из раза в раз приходилось осуществлять правки, а затем снова и снова проверять всё ли там работает. Такой метод тестирования действительно способен вымотать, да и с учётом развития современных методов и подходов к тестированию это объективно не нужно.
Рассмотрим наиболее быстрый и оптимальный способ подключения фреймворка для тестирования GTest к уже существующему C++ проекту на CMake.
Предположим, файловая структура вашего проекта выглядит следующим образом:
\math\sum.h
\math\sum.cpp
CMakeLists.txt
main.cpp
В файле sum.cpp будет определена следующая простая функция вычисления суммы:
#include "sum.g"
int Sum(int a, int b)
{
return a + b;
}
А в заголовке sum.h будет расположена её сигнатура:
#pragma once
int Sum(int a, int b);
Чтобы добавить фреймворк GTest, сначала создадим новую директорию tests и добавим в неё отдельный CMakeTests.txt файл, который будет конфигурировать модуль тестирования:
# GoogleTest
include(FetchContent)
# Загрузка исходного кода GTest
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.18.0
)
# Установка флагов для оптимизации борки библиотеки
set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
set(BUILD_GMOCK OFF CACHE BOOL "" FORCE)
set(gtest_build_tests OFF CACHE BOOL "" FORCE)
set(gtest_build_samples OFF CACHE BOOL "" FORCE)
if (MSVC)
# Сборка GTest с динамической CRT
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
endif()
FetchContent_MakeAvailable(googletest)
# Активируем тестирование
enable_testing()
# Название приложение
set(EXECUTABLE_FILENAME_TEST "app")
add_executable("${EXECUTABLE_FILENAME_TEST}"
"unit/easy-unit.cpp"
)
target_link_libraries("${EXECUTABLE_FILENAME_TEST}"
PRIVATE
app_core
GTest::gtest_main
)
include(GoogleTest)
gtest_discover_tests("${EXECUTABLE_FILENAME_TEST}")
Помимо прочего, следует сразу же в директорию tests добавить файл unit\easy-unit.cpp, в котором можно сразу же описать первый простой Unit-тест:
#include <gtest/gtest.h>
// Сразу подключаем заголовок для вызываемой функции
#include "math/sum.h"
TEST(MathTest, SumPositiveNumbers)
{
EXPECT_EQ(Sum(2, 3), 5);
}
Файловая структура проекта должна быть следующей:
...
\tests\unit\easy-unit.cpp
\tests\CMakeLists.txt
...
С базовой настройкой тестирования почти закончили. Осталось осуществить самый важный шаг - интегрировать модуль тестов в основной CMakeLists.txt файл. Делается это довольно просто:
# Создание статической библиотеки
add_library(app_core STATIC
"math/sum/sum.cpp"
)
# Установка публичного include пути статической библиотеки
target_include_directories(app_core
PUBLIC
${PROJECT_SOURCE_DIR}
)
# Подключение тестового компонента
add_subdirectory(tests)
Статическая библиотека необходима, чтобы в тестовом проекте (который является ответвлением от текущего) нам не приходилось импортировать все зависимости нашего приложения. Иначе каждый раз придётся вручную добавлять большое множество файлов в проект тестирования через инструкцию add_executable.
И да, тестовый проект это отдельный exe-файл, и собирается он отдельно от основного проекта (это стоит учитывать при инфраструктурных задачах).
В результате мы получаем простую интеграцию системы тестирования в проект и можем добавлять различные unit и интеграционные тесты для всевозможной проверки работоспособности нашей системы. Меньше времени на не нужную рутину, больше на программирование и решение задач.
This media is not supported in your browser
VIEW IN TELEGRAM
#из_практического_опыта
Одной из самых сложных задач в начале моего карьерного пути была реализация "простой" системы временных интервалов, которые могли между собой пересекаться (и друг друга поглощать), а также они должны были не мешать друг другу, если этих интервалов на сплошной временной линии будет находиться очень много. Даже пришлось сделать вычисление многочлена Лагранжа 6-го порядка для браузера Firefox, чтобы как можно быстрее выпустить рабочую версию продукта с этими нововведениями (координаты всё никак не хотели правильно рассчитываться на основании уже существующих формул для других браузеров). Возможно, такое решение и было костыльным, однако оно работает и работает хорошо. На какие ухищрения не пойдёшь, чтобы обеспечить кроссбраузерность :)
Одной из самых сложных задач в начале моего карьерного пути была реализация "простой" системы временных интервалов, которые могли между собой пересекаться (и друг друга поглощать), а также они должны были не мешать друг другу, если этих интервалов на сплошной временной линии будет находиться очень много. Даже пришлось сделать вычисление многочлена Лагранжа 6-го порядка для браузера Firefox, чтобы как можно быстрее выпустить рабочую версию продукта с этими нововведениями (координаты всё никак не хотели правильно рассчитываться на основании уже существующих формул для других браузеров). Возможно, такое решение и было костыльным, однако оно работает и работает хорошо. На какие ухищрения не пойдёшь, чтобы обеспечить кроссбраузерность :)
#системное_программирование
https://www.youtube.com/live/eUHeHWZCjz4?si=5rwjh6qVzqZ13Teh
Не так давно наткнулся на занимательное видео, в котором два спикера по очереди разбирают некоторые особенности программирования под ОС Linux через призму их практического опыта (почта mail).
Наиболее интересным (лично для меня) в данном материале был разбор проблемы дефрагментации памяти и методах её исправления.
Хоть сам видеоролик и был выпущен 9 лет назад, однако до сих пор актуален.
https://www.youtube.com/live/eUHeHWZCjz4?si=5rwjh6qVzqZ13Teh
Не так давно наткнулся на занимательное видео, в котором два спикера по очереди разбирают некоторые особенности программирования под ОС Linux через призму их практического опыта (почта mail).
Наиболее интересным (лично для меня) в данном материале был разбор проблемы дефрагментации памяти и методах её исправления.
Хоть сам видеоролик и был выпущен 9 лет назад, однако до сих пор актуален.
YouTube
TechnoLive - Тонкие моменты Linux API (И.Ремень, Д.Исаев)
Базовые возможности API Linux знает, наверное, каждый системный программист. Но так ли все очевидно? Какую из функций, имеющих одинаковое назначение, стоит применять в зависимости от архитектуры вашего приложения? Какие баги могут всплыть из-за необдуманного…
#тестирование
Как вообще тестируется ОС Linux? Несмотря на то, что Linux используется на миллиардах устройств по всему миру, даже ей необходимо своевременное тестирование и обнаружение потенциальных уязвимостей.
Помимо внутренних тестов самого ядра, существует отдельный проект призванный решить насколько корректно работают различные компоненты этой ОС: Linux Test Project (https://github.com/linux-test-project/ltp)
В данном проекте можно найти тестирование любого системного вызова. Например, вот так выглядит один из тестов на открытие файла через системный вызов open:
У интеграционных тестов, как и полагается, есть функции подготовки и очистки среды:
Немного разберём пример этого теста.
TST_EXP_FD_SILENT - выполняет системный вызов open и записывает результат его работы в TST_RET (глобальную переменную). Есть также глобальная переменная TST_PASS, которая отвечает за прохождение всего тела TST_EXP_FD_SILENT. Если TST_PASS = 0, то на каком-то этапе функция преждевременно завершила своё выполнение.
TST_RET, в данном случае, является дескриптором файла. И далее по телу теста используя данный дескриптор файла выполняются дополнительные проверки. В общем-то, ничего сложного.
Изучение Linux API и в целом внутреннее устройство данной ОС через (в том числе) её программные тесты однозначно может принести пользу для понимания как это всё устроено на практике.
Как вообще тестируется ОС Linux? Несмотря на то, что Linux используется на миллиардах устройств по всему миру, даже ей необходимо своевременное тестирование и обнаружение потенциальных уязвимостей.
Помимо внутренних тестов самого ядра, существует отдельный проект призванный решить насколько корректно работают различные компоненты этой ОС: Linux Test Project (https://github.com/linux-test-project/ltp)
В данном проекте можно найти тестирование любого системного вызова. Например, вот так выглядит один из тестов на открытие файла через системный вызов open:
static void verify_open(unsigned int n)
{
struct tcase *tc = &tcases[n];
struct stat buf;
TST_EXP_FD_SILENT(open(tc->filename, tc->flag, tc->mode),
"open() with %s", tc->desc);
if (!TST_PASS)
return;
fd = TST_RET;
SAFE_FSTAT(fd, &buf);
if (!(buf.st_mode & tc->tst_bit))
tst_res(TFAIL, "%s is cleared unexpectedly", tc->desc);
else
tst_res(TPASS, "%s is set as expected", tc->desc);
SAFE_CLOSE(fd);
if (S_ISREG(buf.st_mode))
SAFE_UNLINK(tc->filename);
}
У интеграционных тестов, как и полагается, есть функции подготовки и очистки среды:
static void setup(void)
{
SAFE_MKDIR(TEST_DIR, 0755);
}
static void cleanup(void)
{
if (fd > 0)
SAFE_CLOSE(fd);
}
Немного разберём пример этого теста.
TST_EXP_FD_SILENT - выполняет системный вызов open и записывает результат его работы в TST_RET (глобальную переменную). Есть также глобальная переменная TST_PASS, которая отвечает за прохождение всего тела TST_EXP_FD_SILENT. Если TST_PASS = 0, то на каком-то этапе функция преждевременно завершила своё выполнение.
#define TST_EXP_POSITIVE__(SCALL, SSCALL, ...) \
do { \
TEST(SCALL); \
TST_PASS = 0; \
\
if (TST_RET == -1) { \
TST_MSG_(TFAIL | TTERRNO, " failed", \
SSCALL, ##__VA_ARGS__); \
break; \
} \
if (TST_RET < 0) { \
TST_MSGP_(TFAIL | TTERRNO, " invalid retval %ld",\
TST_RET, SSCALL, ##__VA_ARGS__); \
break; \
} \
\
TST_PASS = 1; \
} while (0)
#define TST_EXP_FD_SILENT(SCALL, ...) \
TST_EXP_POSITIVE__(SCALL, #SCALL, ##__VA_ARGS__)
TST_RET, в данном случае, является дескриптором файла. И далее по телу теста используя данный дескриптор файла выполняются дополнительные проверки. В общем-то, ничего сложного.
Изучение Linux API и в целом внутреннее устройство данной ОС через (в том числе) её программные тесты однозначно может принести пользу для понимания как это всё устроено на практике.
GitHub
GitHub - linux-test-project/ltp: Linux Test Project (mailing list: https://lists.linux.it/listinfo/ltp)
Linux Test Project (mailing list: https://lists.linux.it/listinfo/ltp) - linux-test-project/ltp
#системное_программирование
Довольно часто при разработке приложений на C/C++ под Linux приходится сталкиваться с обработкой глобальной переменной errno. Как правило именно в неё записывают код ошибки большинство системных вызовов Linux. Да и в некоторых местах Windows также использует эту глобальную переменную, как и STL (особенно при работе с файлами).
Формально, в однопоточной среде, определение errno выглядит следующим образом:
Т.е. это просто глобальная переменная, подключаемая через заголовочный файл errno.h.
Однако в многопоточной среде, её определение следует интерпретировать несколько иначе:
[https://git.musl-libc.org/cgit/musl/tree/src/errno/__errno_location.c]
Я не просто так подчеркнул слово "интерпретировать". Всё дело в том, что в каждой библиотеке своя реализация данного определения, но суть у всех одна - показать, что errno - это по сути вызов функции с разыменованием её результата (результат - адрес на конкретный участок памяти потока называемый Thread Local Storage, в которой и содержится значение ошибки). В примере выше это не так очевидно (т.к. в основном вся внутренняя логика скрыта в runtime), но следующие примеры будут более явные.
Такой вызов потокобезопасен, поскольку предполагает, что каждый поток будет обращаться к своему участку памяти, поэтому вы можете быть спокойны при обработке errno в разных потоках. Однако асинхронно-сигнальная среда в Linux не безопасна даже для такого определения errno, и на это стоит обратить внимание.
Для наиболее лучшей интерпретации определения errno через функцию предлагаю ознакомится с этой реализацией:
[https://git.musl-libc.org/cgit/musl/tree/src/errno/__errno_location.c]
В данной реализации сразу видно, что значение errno получается после ссылки на текущий поток. Кстати, такая реализация используется и в однопоточной среде, интерпретировать errno просто как глобальную переменную проще (но не правильно).
Это похоже на реализацию errno в MSVCRT:
[https://github.com/wine-mirror/wine/blob/master/dlls/msvcrt/errno.c]
Довольно часто при разработке приложений на C/C++ под Linux приходится сталкиваться с обработкой глобальной переменной errno. Как правило именно в неё записывают код ошибки большинство системных вызовов Linux. Да и в некоторых местах Windows также использует эту глобальную переменную, как и STL (особенно при работе с файлами).
Формально, в однопоточной среде, определение errno выглядит следующим образом:
extern int errno;
Т.е. это просто глобальная переменная, подключаемая через заголовочный файл errno.h.
Однако в многопоточной среде, её определение следует интерпретировать несколько иначе:
[https://git.musl-libc.org/cgit/musl/tree/src/errno/__errno_location.c]
#include <errno.h>
#include <tls.h>
int *
__errno_location (void)
{
return &errno;
}
libc_hidden_def (__errno_location)
// ...
#define errno (*__errno_location())
Я не просто так подчеркнул слово "интерпретировать". Всё дело в том, что в каждой библиотеке своя реализация данного определения, но суть у всех одна - показать, что errno - это по сути вызов функции с разыменованием её результата (результат - адрес на конкретный участок памяти потока называемый Thread Local Storage, в которой и содержится значение ошибки). В примере выше это не так очевидно (т.к. в основном вся внутренняя логика скрыта в runtime), но следующие примеры будут более явные.
Такой вызов потокобезопасен, поскольку предполагает, что каждый поток будет обращаться к своему участку памяти, поэтому вы можете быть спокойны при обработке errno в разных потоках. Однако асинхронно-сигнальная среда в Linux не безопасна даже для такого определения errno, и на это стоит обратить внимание.
Для наиболее лучшей интерпретации определения errno через функцию предлагаю ознакомится с этой реализацией:
[https://git.musl-libc.org/cgit/musl/tree/src/errno/__errno_location.c]
#include <errno.h>
#include "pthread_impl.h"
int *__errno_location(void)
{
return &__pthread_self()->errno_val;
}
weak_alias(__errno_location, ___errno_location);
В данной реализации сразу видно, что значение errno получается после ссылки на текущий поток. Кстати, такая реализация используется и в однопоточной среде, интерпретировать errno просто как глобальную переменную проще (но не правильно).
Это похоже на реализацию errno в MSVCRT:
[https://github.com/wine-mirror/wine/blob/master/dlls/msvcrt/errno.c]
int* CDECL _errno(void)
{
return &msvcrt_get_thread_data()->thread_errno;
}