13.2. Тестирование проекта
Тестирование является одним из важнейших этапов разработки. Оно позволяет разобрать семантику кода, проверить его работу, а также работу и состояние того информационного пространства, которое этот код создаёт и с которым взаимодействует (глобальные переменные, макросы, переменные окружения).
Выделяют 4 вида тестирования:
модульное — проверяет работу отдельных модулей написанного продукта;
системное — проверяет работу всей системы изолированно от внешних зависимостей;
интеграционное — проверяет работу в рамках среды, в которой она запускается, и на, вообще говоря, не нами задаваемых условиях среды;
приёмочное тестирование — проверяет внешнее воздействие на проект, внесение не встроенных тестов и проверка поведения системы вообще на её взаимодействие с программами.
Объектом обсуждения этой главы является модульное тестирование по дисциплине
xUnit, задающее собственный путь тестирования системы.
В рамках Unit-тестов определён раннер (test runner) — основная программа, запускающая тесты. Он отвечает за проверку подключаемости тестирующего кода, фильтрацию тестов, сбор информации по запускам и т. д. Все тесты (test) объединены в отдельные тестовые комплекты (test case), проверяющие какое-то конкретное свойство проекта. Множества комплектов, описывающих один раздел работы с проектом объединены в тестовые случаи (test suite). Как правило, в один тестовый случай входят тесты, работающие в одинаковых или похожих условиях, а если условия существенно разные (например, бэкенд в виде файла и бэкенд в базе данных) — в отдельные.
Для более точного понимания можно запомнить, что случай показывает, «в каких условиях сломалось?», комплект — «какая функциональность сломалась?", а сам тест — «что именно сломалось?»
Следующий важный блок любого тестирования — подготовка и ликвидация тестируемого окружения — test fixture, или фикстуры. Фикстуры могут создаваться для каждого из уровней тестирования и описывать все временно создаваемые данные и предварительно выполняемые действия для проверки работоспособности системы.
Одним из критериев качества тестирования является test coverage — покрытие, описывающее полноту тестирования программы. Самый простой показатель — процент строк исходного текста проекта, которые были выполнены в ходе тестирования. Более сложные показатели включают в себя проверку различных execution path — путей выполнения кода: например, если в одно и то же место программы можно попасть, предварительно пройдя и клаузу if условного оператора, и клаузу else, нужно иметь два набора тестов соответствующего места.
Одна из классических подсистем тестирования проектов на Си —
Check. Работа с ней также автоматизирована в
autotools, что позволяет встраивать её в проекты и проводить тестирование при сборке пакетов без дополнительных действий.
Добавим тестирование в проект. Для начала укажем зависимость на
check, воспользовавшись тем, что соответствующие проверки пакет
check добавляет в утилиту определения зависимостей
pkgconfig:
@user:
syscall-master/configure.ac
@@ 11,6 11,9 @@ AC_CONFIG_HEADERS([config.h])
# Checks for programs.
AC_PROG_CC
+# Joint pkgconfig library/include check and variable definition.
+PKG_CHECK_MODULES([CHECK],[check])
+
# Checks for libraries.
# Checks for header files.
Поскольку check — это просто библиотека, тесты — это обычные функции на Си. Каждая тестирующая программа состоит из нескольких частей:
сами тесты;
задание тестового комплекта, тестовых случаев, раннера и, возможно, фикстур;
регистрация: какие случаи принадлежат каким комплектам, какие тесты принадлежат каким случаям;
запуск раннера;
освобождение ресурсов.
Рассмотрим минимальный пример с простейшим тестом — проверкой на наличие соответствующей функции:
#include <stdio.h>
#include <assert.h>
#include <check.h>
#include "syscall.h"
START_TEST(parse_arg_incl) {
assert(parse_arg != NULL);
}
END_TEST
int main(int argc, char *argv[]) {
Suite *suite = suite_create("incl");
TCase *testcase = tcase_create("incl");
SRunner *runner = srunner_create(suite);
int ret;
suite_add_tcase(suite, testcase);
tcase_add_test(testcase, parse_arg_incl);
srunner_run_all(runner, CK_ENV);
ret = srunner_ntests_failed(runner);
srunner_free(runner);
return ret != 0;
}
В примере видно, что:
тесты описываются внутри специальных макросов START_TEST/END_TEST (превращаются в функции);
создание объектов тестирования, их регистрация друг в друге, запуск раннера и анализ результатов — отдельные атомарные операции;
раннер создаётся на базе хотя бы одного тестового комплекта (потом туда можно добавить ещё);
по окончании тестирования программист обязан вызвать srunner_free(), освобождая память не только самого раннера, но и рекурсивно всех зарегистрированных в нём объектов.
Ручное составление тестовых файлов — довольно монотонная и долгая задача. Однако большая часть тестового файла вполне может быть сгенерирована, для этого в пакет libcheck входит утилита checkmk. Тестовые файлы пишутся всё так же на Си, но общая часть задаётся директивами, похожими на Си-препроцессор, а в Makefile.am добавляется вызов checkmk:
@user:
syscall-master/tests/Makefile.am
TESTS = include upstream
check_PROGRAMS = include upstream
.ts.c:
checkmk $< > $@
AM_CFLAGS = -I$(top_builddir)/src @CHECK_CFLAGS@
LDADD = $(top_builddir)/src/libsyscall.la @CHECK_LIBS@
Традиционно исходники для checkmk имеют расширение .ts (от test suite), поэтому многие текстовые редакторы пытаются включить подсветку синтаксиса TypeScript — она, конечно, плохо подходит.
В самих тестах используются специальные функции-макросы для проверки значений. Проверка включает в себя работу как с локальными переменными тестов, так и с глобальными значениями, как, например, указатели на функции библиотеки.
Воспроизведём пример выше с использованием макросов, и также добавим проверку на наличие ещё одной функции:
@user:
syscall-master/tests/include.ts
#include <check.h>
#include "syscall.h"
#test include
ck_assert_ptr_nonnull(parse_arg);
ck_assert_ptr_nonnull(lookup);
В более существенном примере проверим работоспособность некоторых функций:
В качестве параметров тестируемых функций могут выступать как локальные переменные отдельного теста, так и глобальные переменные:
@user:
syscall-master/tests/upstream.ts
#include <check.h>
#include "syscall.h"
int res;
unsigned long ulres;
#suite utility
#tcase scomp
#test diff_syscalls
Syscall sys1 = {"write", 0};
Syscall sys2 = {"read", 0};
res = scomp(&sys1, &sys2);
ck_assert_int_gt(res, 0);
#test same_syscalls
Syscall sys1 = {"open", 0};
Syscall sys2 = {"open", 0};
res = scomp(&sys1, &sys2);
ck_assert_int_eq(res, 0);
#tcase lookup
#test found
char name[] = "write";
res = lookup(name);
ck_assert_int_eq(res, 1);
char syscall_name[] = "write";
#suite basic
#tcase parse_arg
#test arg_length
char arg[] = "#hello";
ulres = parse_arg(syscall_name, arg);
ck_assert_uint_eq(ulres, 5);
#test arg_retval
ret_values[12] = 42;
char arg[] = "$12";
ulres = parse_arg(syscall_name, arg);
ck_assert_uint_eq(ulres, 42);
#test arg_number
char arg[] = "100500";
ulres = parse_arg(syscall_name, arg);
ck_assert_uint_eq(ulres, 100500);
Подробнее рассмотрим обновлённый spec-файл:
@user:
syscall-master/.gear/syscall.spec
Name: syscall
Version: 1.1
Release: alt1
Summary: send system calls from your shell
URL: https://github.com/oliwer/syscall
License: ISC
Group: Other
Source0: %name-%version.tar.gz
# Automatically added by buildreq on Fri Aug 08 2025
# optimized out: glibc-kernheaders-generic glibc-kernheaders-x86 gnu-config libgpg-error perl perl-Encode perl-Pod-Escapes perl-Pod-Simple perl-parent perl-podlators sh5
BuildRequires: perl-Pod-Usage libcheck-devel check
%description
Execute a list of raw system calls. All the system calls listed in your system's
unistd.h are supported, with up to 5 arguments. A maximum of 20 calls can be
executed per invocation, each separated by a comma.
Arguments starting by a # symbol are used to give a string length.
For instance, #hello would be evaluated as 5.
Arguments starting by a $ followed by a number from 0 to 19 refer to a previous
system call return code. For instance, $0 refers to to the return code of the
first system call executed.
To display those values, use the echo built-in command.
The echo command can be used like any other system call
to easily display $ or # values, or any string or number.
%prep
%setup
%build
%autoreconf
%configure
%make_build
%install
%makeinstall_std
%check
make check
%files
%_bindir/%name
%_libdir/*
%_man1dir/*
%changelog
* Fri Aug 08 2025 UsamG1t <usamg1t@altlinux.org> 1.1-alt1
- Add xUnit check
* Fri Aug 08 2025 UsamG1t <usamg1t@altlinux.org> 1.0-alt1
- Initial Build
Поскольку исходный текст проекта расположен на отдельном сетевом ресурсе, в spec-файле должно быть добавлено указание на него, для этого используется директива URL. Также, поскольку в проекте уже указана лицензия (о том, какая именно лицензия, а также их разновидности и отличия, будет рассказано в будущей главе), в spec-файле указывается она (заметим, что смена лицензии конкретно в этом случае была бы допустима, но явной необходимости для этого нет). Наконец, к зависимости на генератор документации нужно также добавить сборочные зависимости на пакет check и на его C-библиотеку.
Итоговый Gear-репозиторий выглядит так:
.
├── configure.ac
├── doc
│ ├── Makefile.am
│ └── syscall.pod
├── LICENSE
├── Makefile.am
├── src
│ ├── basic.c
│ ├── globals.c
│ ├── lssyscalls
│ ├── Makefile.am
│ ├── syscall.c
│ ├── syscall.h
│ └── utility.c
└── tests
├── include.ts
├── Makefile.am
└── upstream.ts
.gear/
├── rules
└── syscall.spec
@user
[user@VM syscall-master]$ gear-hsh --lazy
<...>
Executing(%check): /bin/sh -e /usr/src/tmp/rpm-tmp.70349
+ umask 022
+ /bin/mkdir -p /usr/src/RPM/BUILD
+ cd /usr/src/RPM/BUILD
+ cd syscall-1.1
+ make check
<...>
checkmk include.ts > include.c
<...>
PASS: include
PASS: upstream
============================================================================
Testsuite summary for syscall 1.0
============================================================================
# TOTAL: 2
# PASS: 2
# SKIP: 0
# XFAIL: 0
# FAIL: 0
# XPASS: 0
# ERROR: 0
============================================================================
<...>
Wrote: /usr/src/RPM/SRPMS/syscall-1.1-alt1.src.rpm (w2.lzdio)
Wrote: /usr/src/RPM/RPMS/x86_64/syscall-1.1-alt1.x86_64.rpm (w2.lzdio)
Wrote: /usr/src/RPM/RPMS/x86_64/syscall-debuginfo-1.1-alt1.x86_64.rpm (w2.lzdio)
11.68user 11.20system 0:23.99elapsed 95%CPU (0avgtext+0avgdata 24184maxresident)k
760inputs+19696outputs (0major+888811minor)pagefaults 0swaps
[user@VM syscall-master]$
[user@VM syscall-master]$ cp ~/hasher/repo/x86_64/RPMS.hasher/syscall-1.1-alt1.x86_64.rpm ~/hasher/chroot/.in/
[user@VM syscall-master]$ hsh-shell --rooter
@rooter
[root@localhost .in]# rpm -i syscall-1.1-alt1.x86_64.rpm
<13>Aug 8 11:55:52 rpm: syscall-1.1-alt1 1754653927 installed
[root@localhost .in]# which syscall
/usr/bin/syscall
[root@localhost .in]# ls /usr/share/man/man1/syscall.1.xz
/usr/share/man/man1/syscall.1.xz
[root@localhost .in]# cd
[root@localhost ~]# syscall open my.file е101 0755 , write '$0' hello '#hello' , close '$0'
[root@localhost ~]# cat my.file
hello
[root@localhost ~]#