Как написать модульные тесты в простом C? - PullRequest
20 голосов
/ 26 февраля 2010

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

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

Ответы [ 8 ]

20 голосов
/ 26 февраля 2010

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

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

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

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

13 голосов
/ 26 февраля 2010

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

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

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

6 голосов
/ 26 мая 2011

Вы можете использовать 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 голосов
/ 26 февраля 2010

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

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

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

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

3 голосов
/ 29 июня 2016

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

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

#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.

3 голосов
/ 26 февраля 2010

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

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

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

   assert(x > 1);

   // and so on....

}

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

1 голос
/ 26 февраля 2010

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

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

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

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

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

1 голос
/ 26 февраля 2010

я использую assert. Это не совсем фреймворк.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...