Подтвердить что ты не робот

Как написать модульные тесты в простой C?

Я начал копаться в документации GLib и обнаружил, что он также предлагает модульную систему тестирования.

Но как вы могли выполнять модульные тесты на процедурном языке? Или требуется ли программировать OO в C?

4b9b3361

Ответ 1

Тестирование модулей требует только "разрезанных плоскостей" или границ, на которых может быть проведено тестирование. Очень просто проверить функции C, которые не вызывают другие функции, или которые вызывают только другие проверенные функции. Некоторыми примерами этого являются функции, которые выполняют вычисления или логические операции и являются функциональными по своей природе. Функционально в том смысле, что один и тот же вход всегда приводит к одному и тому же выводу. Тестирование этих функций может иметь огромную пользу, хотя это небольшая часть того, что обычно считается модульным тестированием.

Также возможно более сложное тестирование, такое как использование mocks или stub, но это не так просто, как на более динамичных языках или даже объектно-ориентированных языках, таких как С++. Один из способов приблизиться к этому - использовать #defines. Одним из примеров этой статьи является Модульное тестирование приложений OpenGL, в котором показано, как высмеивать вызовы OpenGL. Это позволяет проверить правильность последовательности вызовов OpenGL.

Другой вариант - использовать слабые символы. Например, все функции API MPI являются слабыми символами, поэтому, если вы определяете один и тот же символ в своем собственном приложении, ваша реализация переопределяет слабую реализацию в библиотеке. Если символы в библиотеке не были слабыми, вы получили бы повторяющиеся ошибки символов во время ссылки. Затем вы можете реализовать то, что является фактически макетом всего API MPI C, что позволяет гарантировать правильное согласование вызовов и отсутствие дополнительных вызовов, которые могут вызвать взаимоблокировки. Также можно загрузить слабые символы библиотеки с помощью dlopen() и dlsym(), и при необходимости передать вызов. MPI фактически предоставляет символы PMPI, которые являются сильными, поэтому нет необходимости использовать dlopen() и друзей.

Вы можете реализовать многие преимущества модульного тестирования для C. Это немного сложнее, и может оказаться невозможным получить тот же уровень покрытия, который вы могли бы ожидать от чего-то, написанного на Ruby или Java, но это определенно стоит сделать,

Ответ 2

На самом базовом уровне модульные тесты - это всего лишь биты кода, которые выполняют другие биты кода и сообщают вам, работают ли они как ожидалось.

Вы можете просто создать новое консольное приложение с функцией main(), которая выполнила ряд тестовых функций. Каждый тест вызывает функцию в вашем приложении и возвращает 0 для успеха или другое значение для отказа.

Я бы дал вам примерный код, но я действительно ржавый с C. Я уверен, что есть некоторые фреймворки, которые сделают это немного легче.

Ответ 3

Вы можете использовать libtap, который предоставляет ряд функций, которые могут обеспечить диагностику при неудачном тесте. Пример его использования:

#include <mystuff.h>
#include <tap.h>

int main () {
    plan(3);
    ok(foo(), "foo returns 1");
    is(bar(), "bar", "bar returns the string bar");
    cmp_ok(baz(), ">", foo(), "baz returns a higher number than foo");
    done_testing;
}

Схожая с библиотеками рассылки на других языках.

Ответ 4

Нет ничего объектно-ориентированного относительно тестирования небольших фрагментов кода в изоляции. В процедурных языках вы проверяете функции и их коллекции.

Если вы в отчаянии, и вам нужно будет отчаянно, я ударил немного препроцессора C и основанной на gmake рамки. Он начинался как игрушка и никогда не рос, но я использовал его для разработки и тестирования нескольких проектов среднего размера (10 000+ линий).

Dave Unit Test минимально навязчив, но он может сделать некоторые тесты, которые, как я думал, изначально не были бы возможны для платформы на основе препроцессора ( вы можете потребовать, чтобы определенный фрагмент кода вызывал ошибку сегментации при определенных условиях, и он будет проверять ее для вас).

Это также пример того, почему тяжелое использование препроцессора трудно сделать безопасно.

Ответ 5

Самый простой способ сделать unit test - создать простой код драйвера, связанный с другим кодом, и вызвать каждую функцию в каждом случае... и утверждать значения результатов функций и создавать понемногу... что я все равно делаю это

int main(int argc, char **argv){

   // call some function
   int x = foo();

   assert(x > 1);

   // and so on....

}

Надеюсь, это поможет, С наилучшими пожеланиями, Том.

Ответ 6

Вот пример того, как вы бы выполнили несколько тестов в одной тестовой программе для данной функции, которая могла бы вызвать библиотечную функцию.

Предположим, что мы хотим протестировать следующий модуль:

#include <stdlib.h>

int my_div(int x, int y)
{
    if (y==0) exit(2);
    return x/y;
}

Затем создадим следующую тестовую программу:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <setjmp.h>

// redefine assert to set a boolean flag
#ifdef assert
#undef assert
#endif
#define assert(x) (rslt = rslt && (x))

// the function to test
int my_div(int x, int y);

// main result return code used by redefined assert
static int rslt;

// variables controling stub functions
static int expected_code;
static int should_exit;
static jmp_buf jump_env;

// test suite main variables
static int done;
static int num_tests;
static int tests_passed;

//  utility function
void TestStart(char *name)
{
    num_tests++;
    rslt = 1;
    printf("-- Testing %s ... ",name);
}

//  utility function
void TestEnd()
{
    if (rslt) tests_passed++;
    printf("%s\n", rslt ? "success" : "fail");
}

// stub function
void exit(int code)
{
    if (!done)
    {
        assert(should_exit==1);
        assert(expected_code==code);
        longjmp(jump_env, 1);
    }
    else
    {
        _exit(code);
    }
}

// test case
void test_normal()
{
    int jmp_rval;
    int r;

    TestStart("test_normal");
    should_exit = 0;
    if (!(jmp_rval=setjmp(jump_env)))
    {
        r = my_div(12,3);
    }

    assert(jmp_rval==0);
    assert(r==4);
    TestEnd();
}

// test case
void test_div0()
{
    int jmp_rval;
    int r;

    TestStart("test_div0");
    should_exit = 1;
    expected_code = 2;
    if (!(jmp_rval=setjmp(jump_env)))
    {
        r = my_div(2,0);
    }

    assert(jmp_rval==1);
    TestEnd();
}

int main()
{
    num_tests = 0;
    tests_passed = 0;
    done = 0;
    test_normal();
    test_div0();
    printf("Total tests passed: %d\n", tests_passed);
    done = 1;
    return !(tests_passed == num_tests);
}

Переопределяя assert, чтобы обновить логическую переменную, вы можете продолжить работу, если утверждение не выполняется и выполняется несколько тестов, отслеживая, сколько из них было успешным и сколько неудачно.

В начале каждого теста установите rslt (переменные, используемые макросом assert) в 1, и установите любые переменные, которые управляют вашими заглушками. Если один из ваших заглушек вызван более одного раза, вы можете настроить массивы управляющих переменных, чтобы заглушки могли проверять разные условия на разные вызовы.

Так как многие библиотечные функции являются слабыми символами, их можно переопределить в вашей тестовой программе, чтобы они вызывались вместо них. Перед вызовом функции для тестирования вы можете установить несколько переменных состояния для управления поведением функции заглушки и проверки условий на параметры функции.

В тех случаях, когда вы не можете переопределить это, дайте функции заглушки другому имени и переопределите символ в тестируемом коде. Например, если вы хотите заглушить fopen, но обнаружите, что это не слабый символ, определите свой заглушку как my_fopen и скомпилируйте файл для тестирования с помощью -Dfopen=my_fopen.

В этом конкретном случае проверяемая функция может вызвать exit. Это сложно, поскольку exit не может вернуться к тестируемой функции. Это одно из редких случаев, когда имеет смысл использовать setjmp и longjmp. Вы используете setjmp перед тем, как войти в функцию для тестирования, а затем в окоченном exit вы вызываете longjmp, чтобы вернуться обратно в тестовый файл.

Также обратите внимание, что переопределенная exit имеет специальную переменную, которая проверяет, действительно ли вы хотите выйти из программы и вызывает _exit. Если вы этого не сделаете, ваша тестовая программа может не выйти из строя.

Этот набор тестов также подсчитывает количество попыток и неудачных тестов и возвращает 0, если все тесты прошли и 1 в противном случае. Таким образом, make может проверить наличие ошибок при тестировании и действовать соответственно.

Вышеуказанный тестовый код выведет следующее:

-- Testing test_normal ... success
-- Testing test_div0 ... success
Total tests passed: 2

И код возврата будет 0.

Ответ 7

Я использую assert. Это не действительно каркас, хотя.

Ответ 8

С C он должен идти дальше, чем просто реализовать фреймворк поверх существующего кода.

Одна вещь, которую я всегда делал, - это сделать модуль тестирования (с основным), в котором вы можете выполнить небольшие тесты, чтобы проверить свой код. Это позволяет делать очень малые приращения между циклами кода и тестов.

Большая проблема заключается в написании вашего кода для проверки. Сосредоточьтесь на небольших независимых функциях, которые не зависят от общих переменных или состояний. Попробуйте написать "Функциональный" способ (без состояния), это будет легче протестировать. Если у вас есть зависимость, которая не всегда может быть или медленна (например, база данных), вам может потребоваться написать весь "макет", который может быть заменен вашей базой данных во время тестов.

Целевые цели тестирования модулей по-прежнему применяются: убедитесь, что проверенный код всегда сбрасывается в заданное состояние, постоянно тестируется и т.д.

Когда я писал код на C (назад до Windows), у меня был пакетный файл, который вывел бы редактор, а затем, когда я закончил редактирование и выходил, он будет компилировать, связывать, выполнять тесты, а затем выводить редактор с помощью результаты сборки, результаты тестов и код в разных окнах. После моего перерыва (от минуты до нескольких часов в зависимости от того, что было скомпилировано) я мог просто просмотреть результаты и вернуться к редактированию. Я уверен, что этот процесс можно было бы улучшить в эти дни:)