12 марта 2011

Тесты (Шаг 3.1)

Привет. Давайте писать тесты! Как? Сейчас? Да! Тесты нам помогут определиться с архитектурой, вселят в нас уверенность, подарят нам много часов крепкого и здорового сна.

В CMake есть CTest. Его очень легко использовать. Давайте напишем простой тест для ознакомления с тем как это работает.

cmake_minimum_required (VERSION 2.8)

project(Tests C)

add_executable(test1 test1.c)
add_executable(test2 test2.c)
add_executable(test3 test3.c)
add_executable(test4 test4.c)

enable_testing()

# добавляем тесты в формате ИМЯ_ТЕСТА КОМАНДА АРГУМЕНТЫ_КОМАНДЫ
add_test(Test1 test1)
add_test(Test2 test2)
add_test(Test3 test3)
add_test(Test4 test4 "hello")

Сам тест считается провальным если он вернул что-то кроме 0 или выбросил исключение. Например если не сработает assert (смотри в assert.h).

Вот примеры того, как может выглядеть код тестов:

test1.c

#include <stdio.h>

int
main(int argc, char *argv[])
{
  if (1 == 2)
    return 1;

  return 0;
}

test2.c

#include <stdio.h>

int
main(int argc, char *argv[])
{
  if (2 != 2)
    return 1;

  return 0;
}

test3.c

#include <stdio.h>

int
main(int argc, char *argv[])
{
  if (argc != 1)
    return 1;

  return 0;
}

test4.c

#include <stdio.h>
#include <string.h>

int
main(int argc, char *argv[])
{
  if (argc != 2)
    return 1;

  if (strcmp(argv[1], "hello") != 0)
    return 1;

  return 0;
}

Что бы запустить тесты нужно выполнить команду make test. Правда ведь не сложно?

Можно вполне обойтись и без CTest, мы можем вместо тестов добавить еще одну цель.

add_custom_target("test"
  COMMAND test1
  COMMAND test2
  COMMAND test3
  COMMAND test4 "hello")

Этот вариант удобен тем, что вызовет всех целей которые попадают в зависимости (то есть test1, test2, test3, test4). Если вы не используете CDash, то такой вариант может быть удобнее, правда вывод будет уже не такой красивый, но для этого можно воспользоваться какой-нибудь библиотекой для проведения юнит тестирования.

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

Хватит болтологии. Резюмирую.

  1. Тесты должны быть простыми
  2. Тесты должны быть быстрыми
  3. Тесты должны тестировать код проекта
  4. Тесты должны тестировать только код проекта
  5. Тесты должны тестировать только код проекта, а не сторонние библиотеки используемые в коде

Все остальное плюшки. Плюшки это вкусно, но от них полнеют.

Для тестирования я буду использовать c just test it. Это мною написанные несколько макросов и функция для уменьшения кода теста. Все максимально просто.

/* c just test it
 * Copyright (C) 2011  Alexander A. Prusov
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/* Example:
 * #include "c_just_test_it.h"
 *
 * int i;
 *
 * void
 * set_up()
 * {
 *   i = 1;
 * }
 *
 * void
 * run()
 * {
 *   i++;
 *
 *   begin_assertions();
 *
 *   assert(i == 2);
 *
 *   end_assertions();
 * }
 *
 * void
 * other_run()
 * {
 *   begin_assertions();
 *
 *   assert(0 == 0);
 *
 *   end_assertions();
 * }
 *
 * void
 * tear_down()
 * {
 *   i = 0;
 * }
 *
 * int main(int argc, char** argv)
 * {
 *   run_test("Fail test",       NULL,   run,       tear_down);
 *   run_test("Pass test",       set_up, run,       tear_down);
 *   run_test("Other pass test", NULL,   other_run, NULL);
 *   return 0;
 * }
 */

#ifndef C_JUST_TEST_IT_H__
#define C_JUST_TEST_IT_H__

#include <stdio.h>

typedef void(*function)();

#define assert(expr) do { \
  if (!(expr)) \
    { \
      fprintf(stdout, "%20s",  "FAIL"); \
      fprintf(stdout, "\n%s:%d: %s\n", __FILE__, __LINE__, #expr); \
      ret = 1; \
    } \
  } while(0)

#define begin_assertions() int ret = 0

#define end_assertions() if (!ret) fprintf(stdout, "%20s", "PASS"); \
  fprintf(stdout, "\n")

void
run_test(char *name, function set_up, function run, function tear_down)
{
  fprintf(stdout, ":: %-30s", name);
  if(set_up)
    set_up();
  run();
  if(tear_down)
    tear_down();
}

#endif /* C_JUST_TEST_IT_H__ */

Если вы заметили что функция run_test реализована непосредственно в .h файле, то я все объясню. Изначально с just test it задумывался как простой набор вспомогательных функций и макросов для написания очень простых тестов. Его не нужно компилировать вместе с тестами или пользоваться дополнительными библиотеками. Достаточно просто написать #include "c_just_test_it.h" и вперед, тестировать код. Я считаю что в данном случае это оправдано, но если вы так будете делать везде и всегда, то Брайан Керниган и Деннис Ритчи заплачут горькими слезами.

Продолжение следует...