#
Глава 7: Тестирование вашего кода на Zig
Тестирование, вероятно, любимая запоздалая мысль в мире программирования. Добро пожаловать в главу, где мы сталкиваемся с неудобной реальностью: ваш тщательно продуманный код может быть не таким безупречным, как вам кажется. Но не волнуйтесь, вы в хорошей компании — мы все когда-то отправляли в продакшн баг или два (или пятьдесят), о которых предпочли бы забыть.
В этом поучительном путешествии мы погрузимся во встроенный пакет тестирования Zig — std.testing, потому что, видимо, создатели языка решили, что нам может понадобиться небольшая помощь в ловле собственных ошибок.
Мы рассмотрим, как писать модульные тесты, которые не только подтверждают, что ваш код работает как задумано, но и служат упреждающим ударом против тех ночных сеансов отладки, подпитываемых кофеином и сожалением.
Мы обсудим, как эффективно структурировать ваши тесты, чтобы ваше будущее «я» не ломало голову, о чём вы вообще думали (и думали ли вообще). Также мы разберём утверждения (assertions) — те восхитительные строки кода, которые позволяют вашей программе пассивно-агрессивно сообщать вам, когда что-то пошло совсем не так.
В этой главе мы рассмотрим следующие темы:
- Модульное тестирование: потому что отладка — это форма самоистязания.
- std.testing: не такой уж секретный инструмент Zig для уверенности в коде.
- Структура тестов: организация ваших тестов (чтобы вы могли найти их позже).
- Покрытие тестами.
К концу этой главы вы примете привычку писать тесты вместе с кодом. Не потому, что вам это нравится, а потому, что поймёте: немного усилий на старте лучше, чем душераздирающее отчаяние от поиска бага в разросшейся кодовой базе. Так что засучим рукава и приступим.
В конце концов, если вы не тестируете свой код, кто знает, что он делает?
#
Технические требования
Весь код, показанный в этой главе, можно найти в каталоге Chapter07 нашего репозитория Git: https://github.com/PacktPublishing/Learning-Zig/tree/main/Chapter07.
#
Модульное тестирование: потому что отладка — это форма самоистязания
Мы все были там: смотрим в экран в 3 часа ночи и гадаем, почему наш код ведёт себя как бунтующий подросток. Отладка без тестов — это как искать иголку в стоге сена с завязанными глазами: мучительно, долго и совершенно необязательно.
На сцену выходит модульное тестирование — невоспетый герой разработки ПО. Представьте модульные тесты как вашу личную армию стражей кода, которые ловят баги до того, как те проберутся в продакшн и поставят вас в неловкое положение перед коллегами (или, что ещё хуже, перед пользователями). В Zig написание модульных тестов — это не просто хорошая практика, это стратегия выживания.
#
Зачем вообще заморачиваться с модульными тестами?
Вы можете думать: «Мой код безупречен. Мне не нужны тесты». Ах, сладкая иллюзия самоуверенности. Жёсткая реальность такова: даже лучшие разработчики ошибаются. Модульные тесты выполняют несколько важнейших функций:
- Раннее обнаружение: ловите ошибки, когда их дёшево исправить, а не после того, как они обрушили ваше приложение в дикой среде.
- Документация: предоставляете конкретные примеры того, как ваш код должен работать, избавляя будущего себя от роли археолога в собственной кодовой базе.
- Сетка безопасности при рефакторинге: позволяете вносить изменения с уверенностью, что не вносите новые баги — а кто не любит страховку?
Давайте погрузимся в пример, пока вы не потеряли интерес и не вернулись к пролистыванию мемов. Предположим, у вас есть функция, которая вычисляет квадрат числа:
fn square(x: i32) i32 {
return x * x;
}
Достаточно просто, правда? Но простота умеет маскировать тонкие баги. Давайте напишем модульный тест, чтобы убедиться, что эта функция работает как надо:
const std = @import("std");
test "square function should return the square of a number" {
try std.testing.expect(square(3) == 9);
try std.testing.expect(square(-4) == 16);
try std.testing.expect(square(0) == 0);
}
Вот что мы можем заметить в этом тесте:
- Мы используем std.testing.expect, чтобы утверждать, что наша функция square возвращает правильное значение.
- Мы тестируем положительные числа, отрицательные числа и ноль — охватывая диапазон входных данных, потому что, хотите верьте, хотите нет, у пользователей есть талант вводить тот самый случай, который вы не предусмотрели.
#
Понимание структуры теста
Синтаксис тестирования в Zig прост и понятен — чего не скажешь о некоторых других языках (кашляет Java). Ключевое слово test вводит блок теста, за которым может следовать описательная строка:
test "описание того, что проверяет этот тест" {
// Код теста идёт здесь
}
Внутри блока теста вы можете писать любой код, в конце концов, тесты — это просто функции, которым не нужно объявлять возвращаемый тип или параметры. Неявный возвращаемый тип — anyerror!void, что на элегантном языке означает: ваш тест может вернуть ошибку, чтобы указать на сбой.
Делайте тесты осмысленными
Предостережение: писать тесты, которые всегда проходят, так же полезно, как заваривать чай в шоколадном чайнике. Ваши тесты должны быть спроектированы так, чтобы бросать вызов вашему коду, а не раздувать ваше эго. Учитывайте граничные случаи, недопустимые входные данные и всё, что может заставить ваш код споткнуться.
Давайте расширим нашу функцию square, чтобы она корректно обрабатывала потенциальное переполнение, ведь переполненные целые числа любят портить всем день:
fn safeSquare(x: i32) !i32 {
const ov = @mulWithOverflow(x, x);
if (ov[1] != 0) return error.Overflow;
return ov[0];
}
Теперь давайте напишем тест для этой усовершенствованной функции:
test "safeSquare должен возвращать ошибку при переполнении" {
try std.testing.expectError(error.Overflow, safeSquare(46341));
// 46341 * 46341 превышает максимальное значение для i32
}
Здесь мы намеренно вызываем переполнение, чтобы убедиться, что наша функция корректно его обрабатывает.
Это тот самый проактивный подход к тестированию, который спасает вас от ночных панических атак.
#
Принятие неудач (в тестах, а не в жизни)
Не бойтесь писать тесты, которые завершаются неудачей. На самом деле, наблюдать за тем, как тест падает, может быть странно приятно — это доказательство того, что ваши тесты действительно выполняют свою работу. Используйте неудачи как возможность улучшить свой код. Помните, цель тестирования — не доказать, что ваш код работает, а найти места, где он не работает.
Разработка через тестирование (TDD)
Если вы чувствуете себя особенно авантюрно, вы можете рассмотреть подход разработки через тестирование. TDD подразумевает планирование тестов (или требований) до написания кода, который они проверяют. Это звучит задом наперёд, но заставляет вас критически задуматься о том, что должен делать ваш код, что приводит к более чистым и осмысленным реализациям. Пожалуйста, не делайте этого ради догмы (или чтобы быть крутым), а ради потенциальных преимуществ.
Вот несколько моментов, которые стоит помнить, чтобы избежать распространённых ловушек при работе с тестами:
- Не тестируйте компилятор: Нет необходимости тестировать возможности языка или функции стандартной библиотеки — сосредоточьтесь на своём коде.
- Будьте конкретны: Каждый тест должен проверять конкретное поведение. Если тест падает, вы должны сразу понимать, что пошло не так.
- Сохраняйте независимость тестов: Тесты не должны зависеть от состояния, оставленного другими тестами. Иначе вы потратите больше времени на распутывание зависимостей, чем на исправление багов.
К этому моменту вы, возможно, начинаете проникаться идеей, что тестирование — это не просто утомительная обязанность, придуманная садистскими архитекторами ПО. Но, возможно, вы задаетесь вопросом, как эффективно интегрировать тестирование в ваши проекты на Zig без необходимости изобретать велосипед — или, что ещё хуже, копировать и вставлять шаблонный код до тех пор, пока ваши пальцы не начнут кровоточить.
Не бойтесь, дорогой разработчик, ведь Zig даровал нам std.testing — встроенный фреймворк для тестирования, который избавляет вас от унижения создания собственного тестового харда. Думайте о std.testing как о надёжном напарнике, о котором вы не знали, что он вам нужен — Робин для вашего Бэтмена, но без сомнительных костюмов.
#
std.testing: не такой уж секретный инструмент Zig для уверенности в коде
Давайте погрузимся в std.testing — подарок Zig для тех из нас, кто предпочитает писать код, а не заниматься его отладкой. В этом удобном пространстве собраны утилиты, которые делают написание тестов почти приятным или, по крайней мере, менее болезненным.
#
Зачем использовать std.testing?
- Простота: предоставляет простые функции для типовых задач тестирования, чтобы вы могли сосредоточиться на самих тестах, а не на инфраструктуре.
- Интеграция: бесшовно работает с командой тестирования Zig, делая обнаружение и запуск тестов лёгким делом.
- Диагностика: предлагает полезные сообщения об ошибках и трассировки стека при падении тестов, чтобы вы могли точно определить проблему, не превращаясь в детектива.
- Обнаружение утечек памяти: ведь кто не любит узнавать, что у него утекает память, как через сито?
Давайте вернёмся к нашему предыдущему примеру, но на этот раз воспользуемся большим количеством возможностей std.testing. Предположим, вы написали функцию, которая переворачивает строку потому что, видимо, палиндромы — это новый тренд.
Даже беглого взгляда достаточно, чтобы заподозрить у этой функции проблемы (и вы были бы правы). Давайте напишем тест с помощью std.testing, чтобы подтвердить наши подозрения:
const std = @import("std");
test "reverseString should correctly reverse a string" {
const input = "Zig";
const expected = "giZ";
const actual = reverseString(input);
try std.testing.expectEqualStrings(expected, actual);
}
В этом тесте мы используем std.testing.expectEqualStrings для сравнения ожидаемой и фактической строк.
Также, если строки не совпадают, Zig любезно сообщит нам об этом с подробным сообщением об ошибке, выделяющим расхождение.
std.testing предлагает множество функций, чтобы сделать вашу жизнь с тестированием проще. Давайте рассмотрим некоторые из самых полезных:
try std.testing.expect(x == y);
Если условие ложно, тест завершается неудачей, и Zig сообщает номер строки и выражение, вызвавшее сбой. Это как строгий учитель, который не только ставит вам неправильную оценку, но и точно указывает, где вы ошиблись.
Функция std.testing.expectEqual сравнивает два значения на равенство:
try std.testing.expectEqual(expectedValue, actualValue);
Она безопасна по типам и работает с большинством типов данных, избавляя вас от тонких багов из-за приведения типов.
Функция std.testing.expectEqualStrings специально разработана для сравнения строк, потому что давайте будем честны, именно в работе со строками любят прятаться многие баги:
try std.testing.expectEqualStrings(expectedString, actualString);
Функция std.testing.expectError проверяет, что объединение ошибок содержит конкретную ошибку:
try std.testing.expectError(MyErrorType.SomeError, functionThatMayError());
Это особенно удобно при тестировании функций, которые должны завершаться с ошибкой при определённых условиях, ведь иногда сбой и есть ожидаемый результат.
Допустим, вы создали функцию, которая парсит целое число из строки и возвращает ошибку, если строка некорректна:
const std = @import("std");
fn parseInt(s: []const u8) !i32 {
return std.fmt.parseInt(i32, s, 10);
}
Теперь давайте напишем тест, чтобы убедиться в её корректном поведении:
test "parseInt should return an error for invalid input" {
const invalidInput = "not a number";
const result = parseInt(invalidInput);
try std.testing.expectError(std.fmt.ParseIntError.InvalidCharacter, result);
}
В этом тесте:
- Мы намеренно передаём некорректную строку в parseInt.
- Используем expectError, чтобы проверить, что функция возвращает ожидаемую ошибку.
Если parseInt не вернёт InvalidCharacter, тест завершится неудачей — и это предупредит нас о том, что функция не обрабатывает ошибки так, как задумано.
#
Использование тестового аллокатора
Управление памятью — частый источник багов, особенно в языках, которые дают достаточно верёвки, чтобы повеситься (глядя на тебя, C). Zig предоставляет std.testing.allocator — специальный аллокатор, который отслеживает использование памяти во время тестов.
Вот как вы можете его использовать:
const std = @import("std");
test "memory allocation should not leak" {
var allocator = std.testing.allocator;
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);
// Выполняем операции с буфером...
// В конце теста фреймворк автоматически проверяет наличие утечек.
}
Если вы забудете освободить память, Zig сообщит вам об этом, вместе с трассировкой стека, ведущей к вашей ошибке. Это как личный помощник, который напоминает вынести мусор, только мусор здесь — это утечки памяти, а последствия забывчивости куда серьёзнее.
Когда вы запускаете тесты с помощью команды zig test, вы увидите вывод, указывающий, какие тесты прошли, не прошли или были пропущены. Например:
$ zig test my_tests.zig
1/3 reverseString should correctly reverse a string...FAIL
2/3 parseInt should return an error for invalid input...OK
3/3 memory allocation should not leak...OK
Тесты завершились неудачей. Используйте следующую команду для воспроизведения сбоя:
$ zig test my_tests.zig --test-filter reverseString
Тестовый раннер Zig предоставляет полезную информацию:
- Прогресс тестов: показывает количество выполненных тестов и их статус.
- Детали сбоя: включает имя теста и краткое описание сбоя.
- Команда для воспроизведения: предлагает команду для повторного запуска только упавшего теста, избавляя вас от необходимости просматривать нерелевантный вывод.
#
Фильтрация тестов
Когда у вас большой набор тестов, запуск всех из них может занять много времени. Zig позволяет фильтровать тесты по имени:
zig test my_tests.zig --test-filter parseInt
Это запускает только те тесты, в именах которых содержится «parseInt». Это простой, но эффективный способ сосредоточиться на конкретных областях, не увязая в деталях.
Иногда у вас могут быть тесты, которые ещё не готовы или требуют условий, которые в данный момент не выполняются. Вы можете программно пропустить тест, вернув error.SkipZigTest:
test "этот тест ещё не готов" {
return error.SkipZigTest;
}
Тестовый раннер сообщит о тесте как о пропущенном, и вы сможете вернуться к нему, когда будете готовы разобраться с незавершённой задачей.
#
Максимально эффективное использование std.testing
Чтобы использовать всю мощь std.testing, держите в уме следующие советы:
- Будьте описательны: используйте осмысленные имена тестов, которые описывают проверяемое поведение. Будущий вы (и ваши коллеги) оценят ясность.
- Тестируйте инкрементально: пишите тесты по мере разработки новых функций. Это проще, чем пытаться дорабатывать тесты для запутанной кодовой базы.
- Используйте аллокатор: при тестировании кода, который выделяет память, используйте
std.testing.allocator, чтобы выявлять утечки на ранней стадии. - Используйте утверждения: не экономьте на утверждениях. Чем тщательнее вы тестируете поведение своего кода, тем меньше сюрпризов встретите позже.
К этому моменту вы окунулись в бодрящие воды модульного тестирования с std.testing от Zig. Чувствуете прилив сил? Отлично. Но прежде чем вы начнёте штамповать тесты как белка на кофеине, давайте поговорим об организации. В конце концов, какой толк от набора тестов, если вы не можете их найти, когда всё идёт наперекосяк?
#
Структура тестов: организация ваших тестов (чтобы вы могли найти их позже)
Навигация по неорганизованной кодовой базе примерно так же приятна, как уборка офисного холодильника.
Вы можете найти что-то интересное, но, скорее всего, это будет пахнуть. То же самое касается и ваших тестов. Без правильной структуры вы потратите больше времени на поиски ускользающего тестового случая, чем на исправление бага, который он обнаружил.
Так что давайте направим нашего внутреннего Мари Кондо и наведём порядок в хаосе тестирования.
#
Искусство держать тесты рядом
В Zig преобладающая философия — держать тесты рядом с кодом, который они проверяют. Такой подход не только способствует лучшей организации, но и гарантирует, что тесты и код развиваются вместе в идеальной (или, по крайней мере, терпимой) гармонии.
Написание тестов в том же файле, что и тестируемый код, — это практика, поощряющая связность и ясность. Вот фрагмент, где исходный код и его тесты живут вместе:
const std = @import("std");
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
test "add function should correctly add two numbers" {
try std.testing.expectEqual(5, add(2, 3));
}
Основные преимущества такого подхода:
- Немедленный контекст: тесты находятся прямо рядом с кодом, что облегчает понимание того, как должны использоваться функции.
- Упрощённое сопровождение: когда вы изменяете код, вам напоминают обновить тесты — никаких оправданий.
- Снижение когнитивной нагрузки: не нужно жонглировать множеством файлов или каталогов, всё необходимое находится в одном месте.
Держа тесты рядом с кодом, мы снижаем трение при их написании и поддержке. Это как держать зубную щётку в той же ванной, где вы принимаете душ — удобно и способствует хорошим привычкам.
#
Соглашения об именовании: потому что расплывчатые имена никому не помогают
Вы когда-нибудь часами пытались расшифровать, что должен делать test1.zig? Давайте избежим этой трагедии.
- Будьте описательны Имена тестов должны чётко указывать, что они проверяют:
test "calculateTax applies correct rate for basic items" { /* ... */ }
- Используйте последовательные шаблоны Примите соглашение об именовании, подходящее для вашего проекта, и придерживайтесь его:
test "validateInput with valid data returns true" { /* ... */ }
test "validateInput with invalid data returns false" { /* ... */ }
- Избегайте избыточности Поскольку ключевое слово test уже указывает, что это тест, вам не нужно включать «test» в имена.
Плохой пример:
test "testAdditionFunction" { /* ... */ }
Лучше так:
test "addition function adds two positive numbers correctly" { /* ... */ }
Ещё один хороший вариант:
test "should return the sum when add two positive numbers" { /* ... */ }
Когда в файле накапливается несколько тестов, группировка связанных тестов может улучшить читаемость.
Организуйте тесты по функциональности с помощью комментариев в качестве заголовков разделов:
// Tests for the add function
test "add adds positive numbers" { /* ... */ }
test "add adds negative numbers" { /* ... */ }
// Tests for the subtract function
test "subtract subtracts positive numbers" { /* ... */ }
test "subtract subtracts negative numbers" { /* ... */ }
Эта простая техника помогает вам (и всем остальным, кто читает ваш код) эффективнее ориентироваться в тестах.
Тесты должны быть независимы друг от друга. Если тест B падает из-за того, что тест A оставил систему в плохом состоянии, вас ждёт мир путаницы. Следует избегать общего состояния и использования глобальных переменных, которые сохраняются между тестами. Также помните использовать defer или errdefer, чтобы гарантировать освобождение ресурсов, даже если тест завершится неудачей. Чтобы сделать эту идею более осязаемой, предположим такой тестовый случай:
// loadData handles large inputs
test "a good test name here" {
var allocator = std.testing.allocator;
const data = try loadData(allocator, "large_input.dat");
defer allocator.free(data);
try std.testing.expect(processData(data));
}
#
Использование doctests: убиваем двух зайцев одним выстрелом
Doctests — это тесты, которые одновременно служат примерами в вашей документации. Это отличный способ показать, как предполагается использовать ваш код.
В вашем коде:
/// Reverses the given string.
///
/// ```zig
/// const reversed = reverse("Zig");
/// // reversed == "giZ"
/// ```
pub fn reverse(s: []const u8) []u8 {
// Реализация здесь
}
Когда вы запускаете zig test, Zig автоматически извлекает и выполняет код из документирующих комментариев как тесты. Только убедитесь, что ваши примеры действительно корректны — ничто так не подрывает доверие, как неработающий пример.
Терпение — добродетель, но не в тестировании
Медленные тесты отбивают желание часто их запускать. Если ваши тесты выполняются дольше, чем выпить кофе, вы будете запускать их реже.
Итак, вы написали свой код, добавили несколько утверждений и даже организовали свои тесты с точностью швейцарского часовщика. Чувствуете себя довольно хорошо, не так ли? Но давайте пока не открывать шампанское. Пришло время взглянуть правде в глаза и проверить, выдержит ли ваш код испытания, которые вы собираетесь на него обрушить. В конце концов, непроверенный код — это просто надежда для компилятора.
#
Запуск тестов: момент истины (смешается или нет?)
Zig предоставляет команду zig test, которая компилирует и запускает все тесты в вашем исходном файле. Всё просто:
$ zig test your_code.zig
Да, это действительно так просто. Но давайте разберём, что происходит:
- Компиляция: Zig компилирует ваш код вместе со всеми блоками тестов.
- Выполнение: Он запускает каждый тест, сообщая об успехе или неудаче.
- Обратная связь: Предоставляет подробный вывод для любых неудачных тестов, включая трассировки стека.
Когда вы запускаете тесты, вы увидите вывод, похожий на этот:
1/3 test.addition adds positive numbers...OK
2/3 test.addition handles negative numbers...FAIL
3/3 test.multiplication works correctly...OK
Тесты завершились неудачей. Используйте следующую команду для воспроизведения сбоя:
$ zig test your_code.zig --test-filter "test.addition handles negative numbers"
Давайте разберём предыдущий вывод:
- Прогресс тестов: указывает, какой тест выполняется из общего числа.
- Имена тестов: отражают описания, которые вы (надеемся) предоставили.
- Результаты: OK для успеха, FAIL для неудачи.
- Детали сбоя: если тест не проходит, Zig любезно предоставляет команду для повторного запуска только этого теста.
Неудачный тест — это не конец света, это возможность для просветления (или, по крайней мере, мы так себе говорим). Zig предоставляет подробную информацию, чтобы помочь вам точно определить проблему.
В примере ниже мы видим вывод о сбое:
2/3 test.addition handles negative numbers...FAIL
/home/user/your_code.zig:42:5: 0x123456 in test.addition handles negative numbers (test)
try std.testing.expectEqual(-5, add(-2, -3));
^
Здесь Zig сообщает вам:
- Местоположение: файл и номер строки, где произошёл сбой.
- Функция: конкретная тестовая функция, которая не прошла.
- Трассировка стека: трассировка стека, ведущая к сбою (полезна для более сложных тестов).
Когда у вас есть набор тестов, вы можете захотеть запустить только часть из них, чтобы сосредоточиться на конкретной области.
Вот пример использования флага --test-filter:
$ zig test your_code.zig --test-filter "addition"
Эта команда запускает только те тесты, в именах которых содержится «addition». Это спасательный круг, когда вы итеративно работаете над конкретной функцией и не хотите продираться через нерелевантный вывод тестов.
Неудачные тесты
Помните: неудачный тест — это подарок, шанс улучшить ваш код. Не игнорируйте неудачные тесты и, что ещё хуже, не комментируйте их, чтобы получить «зелёную» сборку. Это всё равно что отключить пожарную сигнализацию, потому что она постоянно пищит. Вместо этого исследуйте причину, исправьте проблему и наблюдайте, как ваш набор тестов зеленеет от зависти (или успеха).
#
Покрытие тестами
Вот в чём загвоздка: в Zig пока нет встроенной поддержки для генерации информации о покрытии кода. Да, вы всё правильно прочитали. Наш новый блестящий язык ещё не одарил нас роскошью встроенных метрик покрытия. Но не отчаивайтесь! Где есть желание (и командная строка), там есть и способ.
Хотя Zig не будет вести вас за руку по ландшафту покрытия, сообщество открытого исходного кода вас поддержит. В системах Linux вы можете использовать внешние инструменты для генерации отчётов о покрытии вашего кода на Zig. Один из таких выдающихся инструментов — kcov.
kcov — это средство тестирования покрытия кода, которое может анализировать скомпилированные исполняемые файлы даже без специальных флагов компиляции. Оно работает, используя отладочную информацию для сопоставления выполнения вашей программы с исходным кодом.
#
Почему kcov?
- Простота использования: не требует масштабных изменений в процессе сборки.
- Без инструментирования компиляции: работает со стандартной отладочной информацией.
- Гибкий вывод: генерирует отчёты о покрытии в различных форматах, включая HTML.
#
Использование kcov: пошагово
Давайте пройдёмся по тому, как вы можете использовать kcov для генерации отчётов о покрытии для ваших проектов на Zig.
Шаг 1: Установите kcov
Для начала вам нужно установить kcov. В большинстве дистрибутивов Linux это так же просто, как:
$ sudo apt-get install kcov
Здесь вы можете найти инструкцию по установке, подходящую для вашей текущей конфигурации.
Шаг 2: Скомпилируйте ваш код с отладочной информацией
Убедитесь, что ваш исполняемый файл Zig скомпилирован с отладочной информацией. Обычно это режим по умолчанию в Debug, но вы можете указать это явно:
$ zig build-exe main.zig
Шаг 3: Запустите ваш исполняемый файл с kcov
Теперь выполните вашу программу с помощью kcov, чтобы собрать данные о покрытии:
$ kcov coverage_output ./main
Замените coverage_output на желаемый каталог для отчёта о покрытии.
Предположим, у вас есть простая программа на Zig main.zig:
const std = @import("std");
pub fn main() !void {
const args = try std.process.args();
_ = args.next(); // Пропускаем имя программы
const arg = args.next() orelse "world";
std.debug.print("Hello, {}!\n", .{arg});
}
Скомпилируйте и сгенерируйте отчёт о покрытии:
$ zig build-exe main.zig
kcov coverage_output ./main Alice
Hello, Alice!
Шаг 4: Просмотрите отчёт о покрытии
Перейдите в каталог coverage_output, и вы найдёте HTML-отчёт, подробно описывающий, какие строки кода были выполнены. Откройте файл index.html в браузере и насладитесь данными.
#
Генерация покрытия для тестов
А как насчёт ваших тестов? Вы знаете, тех усердных маленьких функций, которые должны проверять, что ваш код работает?
Попросите Zig выполнить ваши тесты через kcov, указав опции --test-cmd и --test-cmd-bin:
$ zig test test.zig --test-cmd kcov --test-cmd coverage_output --test-cmd-bin
Эта команда указывает Zig запускать тесты через kcov, помещая вывод покрытия в каталог coverage_output.
Если мы посмотрим на каждый флаг по отдельности, вот чего мы пытаемся достичь с помощью каждого из них:
- --test-cmd kcov: указывает kcov как команду для запуска тестов.
- --test-cmd coverage_output: передаёт coverage_output как аргумент для kcov.
- --test-cmd-bin: сообщает Zig добавить путь к тестовому бинарному файлу в команду kcov.
Для любителей автоматизации вы можете интегрировать генерацию покрытия в ваш скрипт build.zig.
Предположим, у нас есть файл build.zig, который содержит весь необходимый «клей», чтобы покрытие тестов работало как задумано:
const std = @import("std");
pub fn build(b: *std.Build) void {
const mode = b.standardReleaseOptions();
const exe = b.addExecutable("myapp", "src/main.zig");
exe.setBuildMode(mode);
const run_cmd = exe.run();
const enable_coverage = b.option(bool, "coverage", "Enable code coverage") orelse false;
if (enable_coverage) {
exe.setBuildMode(.Debug); // Убедиться, что включена отладочная информация
exe.setTestCommand(&[_][]const u8{
"kcov",
"coverage_output",
"--exclude-pattern=std",
});
exe.setTestStdDir(b.findStdDir());
}
b.default_step.dependOn(&run_cmd.step);
}
Запустите вашу сборку с включённым покрытием:
$ zig build run -Dcoverage
Прежде чем вы броситесь генерировать отчёты о покрытии, имейте в виду:
- Неиспользуемый код не учитывается: компилятор Zig умён — он не компилирует функции, которые не используются. Это означает, что любой не используемый код не будет отображаться в ваших отчётах о покрытии, что может дать вам ложное чувство завершённости.
- Результаты отражают выполненный код: отчёты о покрытии показывают, какие части вашего скомпилированного кода были выполнены. Если определённые пути кода никогда не запускаются во время тестов, они будут помечены как невыполненные.
Сбор данных о покрытии — это одно; эффективное их использование — другое. Вот основные цели, которых мы можем достичь с помощью данных о покрытии:
- Выявление пробелов: ищите области вашего кода, которые не покрыты тестами, и подумайте почему. Это мёртвый код? Он обрабатывает редкие граничные случаи?
- Приоритизация критических путей: сосредоточьтесь на покрытии кода, который является центральным для функциональности вашего приложения.
- Уточнение тестов: используйте отчёты о покрытии для улучшения ваших тестов, гарантируя, что они задействуют все необходимые пути кода.
#
Ограничения и соображения
- Метод с использованием kcov хорошо работает на Linux. Если вы используете Windows или macOS, вам придётся изучить другие инструменты или методы, так как kcov может быть недоступен или не поддерживаться полностью.
- Запуск вашей программы через kcov вносит некоторые накладные расходы. Хотя для целей тестирования это обычно приемлемо, для использования в продакшене это не подходит.
Конечно, Zig не кормит вас с ложечки метриками покрытия кода, но, возможно, это скрытое благо. Это заставляет вас быть более осознанным в стратегии тестирования, лучше понимать свои инструменты и ценить те выводы, которые может дать анализ покрытия.
Так что вперёд, измеряйте неизмеримое и дайте себе ещё один повод спать спокойно по ночам — зная, что ваш код не просто написан, а протестирован, проверен и готов встретить мир.
#
Итоги
Вы познакомились с std.testing — вашим новым лучшим другом в написании и запуске тестов, которые действительно ловят баги до того, как они поймают вас.
Мы обсудили, как эффективно структурировать ваши тесты, сохраняя их организованными и поддерживаемыми, чтобы будущее «я» не проклинало прошлое «я» за небрежный тестовый код. Вы узнали силу утверждений, дающих вашему коду голос громко жаловаться, когда что-то идёт не так. И вы смело встретили момент истины, запустив свои тесты, интерпретировав результаты и интегрировав тестирование в свой рабочий процесс.
Наконец, мы столкнулись с реальностью покрытия тестами, исследовав, как его измерять с помощью таких инструментов, как kcov — даже если Zig не кормит вас с ложечки метриками покрытия. Теперь вы понимаете важность знания того, какая часть вашего кода действительно протестирована, и как это улучшить.
В следующей главе мы рассмотрим способы организации ваших данных в Zig. Мы погрузимся в искусство эффективного структурирования данных, используя мощную систему типов Zig. Мир организации данных ждёт, и пришло время поднять ваши навыки Zig на новый уровень. В конце концов, код — это не только о том, чтобы заставить вещи работать, но и о том, чтобы заставить их работать хорошо.
#
Время теста!
Инструкции: выберите правильный ответ для каждого вопроса или дайте краткий ответ по требованию.
- Почему модульное тестирование важно в разработке программного обеспечения?
- Оно помогает отлавливать ошибки на ранней стадии, когда их дешевле исправить.
- Оно предоставляет документацию о том, как должен работать код.
- Оно позволяет безопасно проводить рефакторинг, гарантируя, что новые изменения не сломают существующую функциональность.
- Всё вышеперечисленное.
- Как в Zig объявляется модульный тест?
- С помощью синтаксиса
fn testName() void {}. - С помощью префикса
unit_test. - С помощью ключевого слова
test, за которым следует необязательное описание. - Импортируя модуль
unittestи записывая тестовые случаи.
- С помощью синтаксиса
- Какой из следующих способов правильный для утверждения равенства двух значений в тесте Zig?
assert(value1 == value2);try std.testing.expectEqual(value1, value2);std.debug.assertEqual(value1, value2);test.expect(value1 == value2);
- Верно или неверно: написание тестов, которые всегда проходят, считается хорошей практикой, потому что это показывает правильность вашего кода. Ответ: Верно или неверно?
- Каково назначение функции
std.testing.expectErrorво фреймворке тестирования Zig?- Утверждать, что функция не возвращает никаких ошибок.
- Проверять, что функция возвращает конкретную ошибку.
- Обрабатывать исключения, выбрасываемые функцией.
- Пропускать тест при возникновении ошибки.
- При организации тестов в Zig какой подход рекомендуется относительно размещения тестового кода?
- Помещать все тесты в отдельный каталог
tests. - Держать тесты близко к коду, который они проверяют, обычно в том же файле.
- Писать тесты в отдельном модуле и импортировать их.
- Включать тесты в функцию
mainвашего приложения.
- Помещать все тесты в отдельный каталог
- Что из перечисленного НЕ является преимуществом хранения тестов в том же файле, что и тестируемый код?
- Немедленный контекст и ясность.
- Упрощённое сопровождение.
- Снижение когнитивной нагрузки.
- Лучшая производительность скомпилированной программы.
- Как можно запустить только подмножество тестов в Zig, соответствующих определённому шаблону или имени?
- Закомментировав тесты, которые не хотите запускать.
- Используя опцию командной строки
--test-filterсzig test. - Изменив тестовый раннер для исключения определённых тестов.
- Разместив тесты в разных файлах и скомпилировав только нужные.
- Каково назначение использования
deferв тестовой функции?- Отложить выполнение теста до завершения всех остальных.
- Гарантировать освобождение ресурсов даже при сбое теста.
- Отложить инициализацию переменных.
- Запланировать выполнение теста через определённое время.
- Верно или неверно: Zig имеет встроенную поддержку генерации информации о покрытии кода. Ответ: Верно или неверно?
- Какой инструмент рекомендуется для генерации отчётов о покрытии кода для кода на Zig в системах Linux?
- gcov
- lcov
- kcov
- covtest
- При использовании kcov с командой zig test какие флаги необходимы для правильной генерации отчётов о покрытии?
- --enable-coverage
- --test-cmd и --test-cmd-bin
- --coverage-output и --coverage-dir
- --kcov и --kcov-output
- Что такое doctest в Zig и как он используется?
- Тест, который автоматически генерируется компилятором.
- Тест, который также служит документацией, встроенной в комментарии к коду.
- Тест, который проверяет утечки памяти и управление ресурсами.
- Тест, который откладывается до времени выполнения.
- Верно или неверно: допустимо, чтобы тесты в Zig зависели от состояния, оставленного другими тестами. Ответ: Верно или неверно?
- Для чего используется функция error.SkipZigTest в системе тестирования Zig?
- Чтобы указать, что тест прошёл.
- Чтобы программно пометить тест как пропущенный.
- Для обработки непредвиденных ошибок в тестах.
- Для повторного запуска неудачного теста.
- Что из перечисленного является хорошей практикой при именовании тестов в Zig?
- Использовать расплывчатые имена вроде test1, test2 и т. д.
- Включать слово «test» в каждое имя теста.
- Использовать описательные имена, которые чётко указывают, что проверяет тест.
- Делать имена тестов как можно короче, чтобы сэкономить место.
- Какова роль std.testing.allocator в системе тестирования Zig?
- Это аллокатор памяти, оптимизированный для производительности в продакшн-коде.
- Он отслеживает использование памяти во время тестов для обнаружения утечек.
- Он предоставляет функции для выделения и освобождения ресурсов в тестах.
- Он заменяет стандартный аллокатор при запуске тестов.
- Верно или неверно: в Zig можно фильтровать тесты для запуска по их имени с помощью опции --test-filter. Ответ: Верно или неверно?
- Почему вы можете выбрать написание тестов, которые предназначены для сбоя?
- Чтобы намеренно сломать сборку и протестировать CI-пайплайн.
- Чтобы убедиться, что ваша система тестирования корректно сообщает о сбоях.
- Чтобы процент покрытия кода казался ниже.
- Потому что писать неудачные тесты проще.
- Объясните назначение использования std.testing.expect в тесте Zig.
#
Ответы
-
- Всё вышеперечисленное.
-
- С помощью ключевого слова
test, за которым следует необязательное описание.
- С помощью ключевого слова
-
try std.testing.expectEqual(value1, value2);
- Неверно.
-
- Чтобы проверить, что функция возвращает конкретную ошибку.
-
- Держать тесты рядом с кодом, который они проверяют, обычно в том же файле.
-
- Лучшая производительность скомпилированной программы.
-
- Использование опции командной строки
--test-filterсzig test.
- Использование опции командной строки
-
- Чтобы гарантировать освобождение ресурсов даже при сбое теста.
- Неверно.
-
- kcov
-
--test-cmdи--test-cmd-bin
-
- Тест, который также служит документацией, встроенной в комментарии к коду.
- Неверно.
-
- Чтобы программно пометить тест как пропущенный.
-
- Использовать описательные имена, которые чётко указывают, что проверяет тест.
-
- Он отслеживает использование памяти во время тестов для обнаружения утечек.
- Верно.
-
- Чтобы убедиться, что ваша система тестирования корректно сообщает о сбоях.
- Для того чтобы утверждать, что заданное условие истинно; если условие ложно, тест завершается неудачей.