четверг, 10 сентября 2009 г.

The big class unittest

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

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

Есть большой класс... Назовем его CBigApp... 173 публичных метода, еще 40 частных. Видимо для того, чтобы с ним было проще работать реализация разнесена приблизительно на 20 файлов... Ну чтож, это облегчает нашу задачу.

А задача заключается в том, что приложение почему-то перестало импортировать конфигурацию от предыдущей версии. Хороший момент для написания первого юниттеста! :)

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

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

Для проверки импорта конфигурации нам вполне неплохо подходит одна функция - назовем ее import_config, хоят по легенде это сделано давным давно, в 19-м веке. :) Эта функция практически не пользуется другими методами класса, кроме функции convert_config, которая в свою очередь дергает несколько мелких функций по конвертации. Все эти функции не зависят от остального содержимого класса... одна беда - они private.

Как говорят великие, такой набор функций - хороший кандидат на извлечение класса по виду ответственности.

Призовем на помощ препроцессор. Поскольку оригинальное описание класса нас совершенно не устраивает - мы его прячем:
namespace hidden {
#include "CBigApp.h"
}
Если мы этого не сделаем, то оно заинклюдится из сишного файла, который мы хотим заинклюдить к себе. Но для начала мы опишем свою версию класса, и делать это мы будем в cьюте, чтобы изолировать его от взаимозависимостей, ведь у нас как бы появятся свои версии функций. Неизбежны проблемы при линковке тестового приложения. Сьют позволяет изолировать эти манипуляции.
BOOST_AUTO_TEST_SUITE(suiteCBiGApp_ImportConfig)

struct CBigApp {
void import_config(const archive &aconfig, const std::string &config_file);
void convert_config(const std::string &config_file, u_int version);
void fix_10_value(const std::string &value &);
void fix_21_value(const std::string &value &);
void fix_22_value(const std::string &value &);
};
Это собственно наше тестовое видение класса. Оно еще не окончательное и нам еще придется с ним повозиться. Прототипы мы без искажений скопировали из CBigApp.h. Ну а теперь подтягиваем реализацию.
#include "Import.cpp"
Может так сложиться, что помимо вышеописанных функций в этом файле реализуются и другие. Тогда и их прототипы необходимо вписать в тестовый класс.

Может быть эта реализация ссылается на другие функции класса, описанные в другом файле. Их тоже можно описать в структуре с необходимой реализацией.
void create_minimal_config(const std::string &)
{ throw logic_error("Не используется"); }
Помимо того наверняка будет так, что в этом файле включаются и другие инклюды - все эти включения, в том числе и стандартные необходимо перенести в начало нашего юниттест-файла, чтобы они первый раз включились в глобальном пространстве имен, второй раз они уже не будут включаться.

После этого у нас появилась возможность вызвать тест...
BOOST_AUTO_TEST_CASE(testImport10)
{
CBigApp app;
BOOST_REQUIRE_NO_THROW(app.import_config(cfg10, "config_file-1.0.cfg"));
::unlink("config_file-1.0.cfg");
}
Да, я знаю, что использовать файловую систему в юниттестах - плохо... но лучше иметь юниттесты, которые используют файловую систему (базы данных, подключения по сети), чем не иметь никаких. Для успокоения совсети их можно назвать функциональными. :)

В тестировании можно пойти дальше, объявив некоторые из методов как virtual, и заменить их в процессе тестирования на что-то удобное для себя:
BOOST_AUTO_TEST_CASE(testImport21)
{
struct inCBigApp : public CBigApp {
virual fix_10_value(const std::string &value &)
{ throw logic_error("Не должен вызываться"); }
} app;
BOOST_REQUIRE_NO_THROW(app.import_config(cfg21, "config_file-2.1.cfg"));
::unlink("config_file-2.1.cfg");
}

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

Вроде пока все.