Разделы портала

Онлайн-тренинги

.
Тестирование в свете Экстремального Программирования. Часть 1. Практика использования
29.09.2008 19:05

Авторы: Сардарян Рубен, Новичков Александр
Материал предоставлен компанией CMconsult

Данная статья посвящена достаточно интересному направлению в индустрии разработки и тестирования программного обеспечения, а именно экстремальному программированию. Про общие идеологические принципы ХР писалось много и очень много, поэтому мы сразу решили не описывать то, что и так уже есть — основу ХР. Данная статья — это практика использования.

 Статья состоит из двух частей:

  • Практика использования;
  • Экстремальное программирование в IBM Rational Unified Process.

Если с первой частью все в порядке, то вторая может представлять особый интерес, поскольку, как многие знают, у XP и RUP немного разные подходы к разработке и тестированию, и, вообще, они далеко не братья и даже не родственники... Но есть одно "но"! Так как RUP это не догма, а свод рекомендаций, а инструментальные средства Rational могут ГОРАЗДО больше, чем описано в RUP'е, то и такое возможно.

Если вас заинтересовала данная тематика, то к данной статье есть презентация, которую можно получить, послав заявку на наш почтовый ящик Этот e-mail адрес защищен от спам-ботов, для его просмотра у Вас должен быть включен Javascript

Введение

Экстремальное программирование (extreme programming, XP) в настоящий момент является одной из наиболее интересных и «горячих» методологий разработки программного обеспечения. Представляя собой конгломерат простых и хорошо работающих методик, XP помогает разрабатывать качественное программное обеспечение в установленные сроки.

В последнее время XP и входящие в её состав методики начинают всё больше интересовать широкие массы. В России даже издали переводы нескольких западных книг по экстремальному программированию. После прочтения этих книг у читателя в общем складывается достаточно правдоподобное представление об XP, однако до его практического применения в работе ещё достаточно далеко. В этой статье я попытаюсь продемонстрировать практику применения одной из методик, стараясь провести вас дальше в мир экстремального программирования.

Одним из важнейших аспектов XP является автоматизированное тестирование. Основными особенностями тестирования по этой методологии являются:

  • Написание тестов до начала реализации;
  • Полная автоматизация процесса тестирования.

В данной статье рассказывается о том, как организовать такое тестирование на языке C++. Это достаточно просто сделать, используя библиотеку CppUnit, созданную по образу и подобию знаменитой JUnit для Java. Отметим, что в статье будет использован объектно-ориентированный подход к разработке. Это абсолютно не означает, что данным методом нельзя тестировать структурные программы, просто, ООП уже давно стало для меня привычным образом мышления. Библиотека CppUnit также представляет собой иерархию классов.

Краткий обзор архитектуры CppUnit

Чтобы нам было легче, вначале проведём легкий экскурс в архитектуру CppUnit. Наверняка, не всё будет понятным с первого раза, но у вас сложится некоторое базовое представление о библиотеке и методах её функционирования.

На рисунке 1 представлена диаграмма основных классов CppUnit, на которых мы будем строить свой процесс тестирования.


Рисунок 1

Расскажем (пока, в общих чертах) для чего нужен каждый класс.

Test является абстрактным базовым классом для всех тестовых классов. Он нужен, для того, чтобы можно было полиморфно связывать в иерархии различные тестовые объекты.

TestCase является основным тестовым классом – базовой единицей тестирования. Класс TestCase отражает единичный тестовый случай (что, впрочем, видно из его названия). Чтобы его использовать, нужно наследоваться от этого класса и переопределить метод run().

Однако чаще всего, этого не приходится делать, т.к. для создания объектов типа TestCase используется шаблон TestCaller и методы класса TestFixture. TestFixture содержит методы setUp() и tearDown() для создания и уничтожения экземпляров объектов, подлежащих тестированию. TestCaller создаёт экземпляры типа TestCase, вызывающие указанную функцию при вызове run() и использующие объекты, создающиеся указанным TestFixture.

TestSuite представляет собой контейнер множества тестов. TestSuite содержит массив указателей на объекты типа Test, поэтому в него можно помещать как объекты типа TestCase, так и объекты типа TestSuite, тем самым создавая иерархии тестовых случаев любого типа. При этом, при вызове метода run() базового TestSuite-объекта, рекурсивно будут выполнены все тесты, находящиеся в иерархии.

TestRunner также является контейнером тестовых объектов, он является менеджером всего жизненного цикла тестов: от создания тестовых объектов, до их уничтожения и выдачи результатов. TestRunner показывает прогресс (текущее состояние) процесса тестирования и выдает суммарный результат в конце. Возможно выполнение, как всех тестов, содержащихся в контейнере типа TestRunner, так и только одного из них.

Данные о проведенном функцией run() тестировании (ошибки и исключения), заносятся в объект типа TestResult. Для вывода результатов используется классы, наследованные от класса Outputter: CompilerOutputter — для связи с интегрированной средой разработки, TextOutputter — для простого текстового вывода результатов, и XmlOutputter — для вывода результатов в формате XML.

Постановка задачи

Итак, приступим. Например, мы разрабатываем некое приложение и нам нужно создать класс комплексного числа (используем пример, поставляемый вместе с CppUnit), и тесты к этому классу. Мы специально берём такой простой пример, чтобы сосредоточится на процессе тестирования и не отвлекаться на сложность приложения.

Как поступают при традиционных методах разработки? Проектируется класс, его интерфейс , члены, методы, это всё реализуется в коде, и дальше проверяется в работе. Если работает и нету «багов», то всё окей.

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

Построение простейших тестов

Перейдём к практике. Допустим, нам нужно протестировать наш код, чтобы быть уверенным в его работе. Для этого нужно наследовать свой класс от класса CppUnit::TestCase, и переопределить виртуальную функцию runTest(). Для проверки логических значений используется макрос CPPUNIT_ASSERT(bool). Чтобы проверить опе-ратор равенства для класса комплексных чисел, можно использовать следующий код.


class ComplexNumberTest : public CppUnit::TestCase {
public:
ComplexNumberTest( std::string name )
: CppUnit::TestCase( name ) {}

void runTest() {
CPPUNIT_ASSERT( Complex (10, 1) == Complex (10, 1) );
CPPUNIT_ASSERT( !(Complex (1, 1) == Complex (2, 2)) );
}
};

Сейчас, если мы попробуем откомпилировать этот пример, мы получим минимум два сообщения об ошибках: у нас не определён класс Complex и оператор == для него. Без проблем, добавим пустышки, которые позволят откомпилировать проект:

class Complex
{
friend static bool operator==(const Complex& lhs, const Complex& rhs);
};

bool operator==(const Complex& lhs, const Complex& rhs)
{
return true;
}

Теперь проект откомпилируется, но если мы попробуем запустить тесты, то они выдадут ошибку. Чтобы они давали «зелёный свет», нужно написать нормальный оператор ==:

class Complex
{
public:
Complex( double r, double i = 0 ) : real(r), imag(i) { }

friend static bool operator==(const Complex& lhs, const Complex& rhs);

private:
double real;
double imag;
};

bool operator==(const Complex& lhs, const Complex& rhs)
{
return ((lhs.real == rhs.real) && (lhs.imag == rhs.imag));
}

Если мы теперь откомпилируем и запустим наш проект, то тесты сработают.

Добавим оператор сложения для нашего класса комплексных чисел. На этом этапе нам можно будет воспользоваться свойствами TestFixture. Для этого нам нужно наследоваться от этого класса, либо от класса TestCase (см. диаграмму классов – рис. 1), завести переменные для экземпляров нашего класса и переопределить функции setUp() и tear-Down():

class ComplexTestCase : public TestCase
{
private:
Complex *m_10_1, *m_1_1, *m_11_2;

public:
void setUp()
{
m_10_1 = new Complex( 10, 1 );
m_1_1 = new Complex( 1, 1 );
m_11_2 = new Complex( 11, 2 );
}

void tearDown()
{
delete m_10_1;
delete m_1_1;
delete m_11_2;
}
};

Теперь, когда у нас есть объекты, как же организовать проверку отдельных тестовых случаев, проверяющих какие-то отдельные свойства объекта? Это делается при помощи TestFixture и TestCaller. Нужно завести в классе, наследованным от TestFixture (TestCase), функцию для тестового случая, и создать при помощи TestCaller из этой функции экземпляр класса TestCase.

class Complex
{
public:
Complex( double r, double i = 0 ) : real(r), imag(i) { }

Complex operator +(const Complex& other);
friend static bool operator==(const Complex& lhs, const Complex& rhs);

private:
double real;
double imag;
};

Complex Complex::operator +( const Complex &other )
{
Complex result(real + other.real, imag + other.imag);
return result;
}

bool operator==(const Complex& lhs, const Complex& rhs)
{
return ((lhs.real == rhs.real) && (lhs.imag == rhs.imag));
}


class ComplexTestCase : public TestCase
{
private:
Complex *m_10_1, *m_1_1, *m_11_2;

public:
void setUp()
{
m_10_1 = new Complex( 10, 1 );
m_1_1 = new Complex( 1, 1 );
m_11_2 = new Complex( 11, 2 );
}

void tearDown()
{
delete m_10_1;
delete m_1_1;
delete m_11_2;
}

void testEquality()
{
CPPUNIT_ASSERT( *m_10_1 == *m_10_1 );
CPPUNIT_ASSERT( !(*m_10_1 == *m_11_2) );

Complex* pTest = new Complex(10, 1);
CPPUNIT_ASSERT_EQUAL( *m_10_1, *pTest);
}

void testAddition()
{
Complex test_11_2 = *m_10_1 + *m_1_1;
CPPUNIT_ASSERT( *m_10_1 + *m_1_1 == *m_11_2 );
CPPUNIT_ASSERT_EQUAL( test_11_2, *m_11_2);
}
};

Мы добавили оператор сложения в класс Complex и функции, тестирующие опера-тор равенства (testEquality()) и сложения (testAddition()) в класс ComplexTestCase. Теперь, мы можем создать единичный тестовый случай для функции testEquality(), например:

CppUnit::TestCaller<ComplexNumberTest> test( "testEquality",
&ComplexNumberTest::testEquality );

CppUnit::TestResult result;
test.run( &result );

Создание иерархий тестов

Теперь, когда у нас есть все нужные нам тесты, как сделать так чтобы они все вызывались автоматически? Или как составить несколько иерархий тестов, чтобы можно было легко между ними переключаться? Для этого используется класс TestSuite. Он представляет собой контейнер указателей на объекты типа Test.

Вот как это делается:

CppUnit::TestSuite suite;
CppUnit::TestResult result;
suite.addTest( new CppUnit::TestCaller<ComplexNumberTest>(
"testEquality",
&ComplexNumberTest::testEquality ) );
suite.addTest( new CppUnit::TestCaller<ComplexNumberTest>(
"testAddition",
&ComplexNumberTest::testAddition ) );
suite.run( &result );

Объекту типа TestSuite можно передавать не только указатели на TestCase, но и на любые объекты типа Test, в том числе и на сами объекты типа TestSuite.

Для сбора и выдачи результатов используется класс TestRunner. Например, в нашем случае можно для удобства поместить код создания TestSuite в класс ComplexNum-berTest:

public:
static CppUnit::Test *suite()
{
CppUnit::TestSuite *suiteOfTests = new CppUnit::TestSuite(
"ComplexNumberTest" );
suiteOfTests->addTest( new CppUnit::TestCaller<ComplexNumberTest>(
"testEquality",
&ComplexNumberTest::testEquality ) );
suiteOfTests->addTest( new CppUnit::TestCaller<ComplexNumberTest>(
"testAddition",
&ComplexNumberTest::testAddition ) );
return suiteOfTests;
}

А потом в функции main использовать TestRunner:
int main( int argc, char **argv)

{
CppUnit::TextUi::TestRunner runner;
runner.addTest( ExampleTestCase::suite() );
runner.addTest( ComplexNumberTest::suite() );
runner.run();
return 0;
}

TestRunner выполнит все тесты. Если всё окей, то он выдаст соответствующее сообщение, если же что-то не так, будет выдана следующая информация:

Имя тестового случая (TestCase), который не выполнился;
Имя файла, который содержал тест;
Номер строки, в которой произошел сбой/ошибка;
Весь код, который находился в скобках макроса CPPUNIT_ASSERT().
CppUnit различает два типа неполадок: сбои и ошибки. Сбоем считается сбой, который был предусмотрен макросом CPPUNIT_ASSERT(). Ошибками называются непредусмотренные проблемы, такие как деление на ноль, или исключения C++.

Интеграция с процессом сборки

Теперь, когда у нас есть наши тесты, почему бы не соединить их с процессом сборки приложения? Для этого нужно сделать так, чтобы наше тестовое приложение возвра-щало бы ненулевое значение при ошибке:

int main( int argc, char **argv)
{
CppUnit::TextUi::TestRunner runner;
runner.addTest( ExampleTestCase::suite() );
runner.addTest( ComplexNumberTest::suite() );
bool wasSuccessful = runner.run();
return wasSuccessful;
}

Теперь нужно, чтобы приложение выполнялось после компиляции. Например, в Visual C++ это можно сделать, в разделе Project Settings/Post-build steps

Резюме

Вот, собственно, и всё для начала. Надеюсь, этого маленького эссе вам хватит, для того, чтобы начать работу с CppUnit. Эта библиотека, на самом деле немного сложнее, и гораздо более функциональней, чем может показаться после прочтения данной статьи. Однако нашей целью было познакомить вас с библиотекой, а не писать подробное руководство. Надеюсь, нам это удалось. Удачи и успехов!