Product SiteDocumentation Site

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 ~]#