Вместо QTest использую Google Test: http://www.prog.org.ru/topic_26944_0.html
Привет!
Я считаю, что я крайне небрежно написал следующий код. Так как мне приходиться править данные аж в трёх местах.
Сначала объясню ситуацию. У меня есть функция, которая принимает число из диапазона [5; 4*10^5] и это число должно заканчиваться на цифру 5. Функция возвращает аргумент в квадрате.
В случае, если не выполняется соглашение вызова, то функция выбрасывает исключения. Какие именно, это неважно. К проблеме это не относится.
Я написал костыли для отлавливания исключений в QTest. Но когда я хочу добавить данные для тестирования, то мне приходиться добавлять (править) в трёх местах:
1) Там, где я добавляю данные: входные данные и ожидаемые результаты:
C++ (Qt)
void FiveAndFive::testCase1_data()
{
QTest::addColumn<int>("number");
QTest::addColumn<double>("expected");
QTest::newRow("fiveAndFive_01") << 1 << 0.0;
QTest::newRow("fiveAndFive_02") << 0 << 0.0;
QTest::newRow("fiveAndFive_03") << 4 << 0.0;
QTest::newRow("fiveAndFive_04") << 5 << 25.0;
QTest::newRow("fiveAndFive_05") << 6 << 0.0;
QTest::newRow("fiveAndFive_06") << 399995 << 159996000025.0;
QTest::newRow("fiveAndFive_07") << 400000 << 160000000000.0;
QTest::newRow("fiveAndFive_08") << 400001 << 0.0;
QTest::newRow("fiveAndFive_09") << 400005 << 0.0;
}
2) Там, где я проверяю на исключения:
C++ (Qt)
try {
double actual = fiveAndFive( number );
bool result = qFuzzyCompare( actual, expected, delta );
switch ( number ) {
case 0:
case 1:
case 4:
case 6:
case 400001:
case 400005:
QVERIFY2( false, "Failure exception." );
break;
default:
QString msg = QString("\nActual: %1"
"\nExpected: %2"
"\nDelta: %3").arg(actual).arg(expected).arg(delta);
QVERIFY2( result, msg.toStdString().c_str() );
break;
}
3) И там, где отлавливаю исключение:
C++ (Qt)
catch ( const LogicError& e ) {
if ( number != 0 &&
number != 1 &&
number != 4 &&
number != 6 &&
number != 400001 &&
number != 400005
)
{
QVERIFY2( false, "Failure exception." );
} else {
QVERIFY2( true, "" );
}
} catch (...) {
QVERIFY2( false, "Uncaught exception.");
}
Я читал, что если возникают такие зависимости (что приходиться править в друх или более местах), то это означает, что код плохо написан. Я ума не приложу, как тут можно обойти правку в трёх местах.
Вот весь код.
Первый проект создавал, как Qt Unit TestКод, который тестирует функцию:
tst_FiveAndFive.cpp
C++ (Qt)
#include <QString>
#include <QtTest>
#include <stdexcept>
#include <sstream>
class LogicError : public std::logic_error {
public:
LogicError( int argument ) : std::logic_error( "" ), m_argument( argument ) {
}
virtual const char *what( ) const throw () {
return m_message.c_str( );
}
virtual ~LogicError( ) throw () {
}
protected:
int m_argument;
std::string m_message;
};
class InvalidArgument : public LogicError {
public:
InvalidArgument( int argument ) : LogicError( argument ) {
std::stringstream stream;
stream << argument;
m_message = "Argument " + stream.str( ) + " mush be multiple of 5";
}
};
class OutOfRange : public LogicError {
public:
OutOfRange( int argument, int beginOfRange, int endOfRange ) : LogicError( argument ) {
std::stringstream stream;
std::string str_argument, str_beginOfRange, str_endOfRange;
stream << argument;
str_argument = stream.str();
stream.str("");
stream << beginOfRange;
str_beginOfRange = stream.str();
stream.str("");
stream << endOfRange;
str_endOfRange = stream.str();
stream.str("");
m_message = "Argument " + str_argument + " don't hit on the range [" +
str_beginOfRange + ", " + str_endOfRange + "]";
}
};
double fiveAndFive( int number ) throw (InvalidArgument, OutOfRange);
class FiveAndFive : public QObject
{
Q_OBJECT
public:
FiveAndFive();
static inline bool qFuzzyCompare(double p1, double p2, double delta)
{
return (qAbs(p1 - p2) <= delta * qMin(qAbs(p1), qAbs(p2)));
}
private Q_SLOTS:
void testCase1_data();
void testCase1();
};
FiveAndFive::FiveAndFive()
{
}
void FiveAndFive::testCase1_data()
{
QTest::addColumn<int>("number");
QTest::addColumn<double>("expected");
QTest::newRow("fiveAndFive_01") << 1 << 0.0;
QTest::newRow("fiveAndFive_02") << 0 << 0.0;
QTest::newRow("fiveAndFive_03") << 4 << 0.0;
QTest::newRow("fiveAndFive_04") << 5 << 25.0;
QTest::newRow("fiveAndFive_05") << 6 << 0.0;
QTest::newRow("fiveAndFive_06") << 399995 << 159996000025.0;
QTest::newRow("fiveAndFive_07") << 400000 << 160000000000.0;
QTest::newRow("fiveAndFive_08") << 400001 << 0.0;
QTest::newRow("fiveAndFive_09") << 400005 << 0.0;
}
void FiveAndFive::testCase1()
{
QFETCH( int, number );
QFETCH( double, expected );
double delta = 0.0001;
try {
double actual = fiveAndFive( number );
bool result = qFuzzyCompare( actual, expected, delta );
switch ( number ) {
case 0:
case 1:
case 4:
case 6:
case 400001:
case 400005:
QVERIFY2( false, "Failure exception." );
break;
default:
QString msg = QString("\nActual: %1"
"\nExpected: %2"
"\nDelta: %3").arg(actual).arg(expected).arg(delta);
QVERIFY2( result, msg.toStdString().c_str() );
break;
}
} catch ( const LogicError& e ) {
if ( number != 0 &&
number != 1 &&
number != 4 &&
number != 6 &&
number != 400001 &&
number != 400005
)
{
QVERIFY2( false, "Failure exception." );
} else {
QVERIFY2( true, "" );
}
} catch (...) {
QVERIFY2( false, "Uncaught exception.");
}
}
QTEST_APPLESS_MAIN(FiveAndFive)
#include "tst_FiveAndFive.moc"
Файл проекта:
acmp_0003_five_and_five_tests.pro
C++ (Qt)
#-------------------------------------------------
#
# Project created by QtCreator 2014-04-04T15:57:01
#
#-------------------------------------------------
QT += testlib
QT -= gui
TARGET = tst_FiveAndFive
CONFIG += console
CONFIG -= app_bundle
TEMPLATE = app
SOURCES += tst_FiveAndFive.cpp
SOURCES += ../acmp_0003_five_and_five/main.cpp
DEFINES += SRCDIR=\\\"$$PWD/\\\"
Второй проект создавал, как Empty Qt ProjectЭто файл, в котором находится тестируемая функция (для запуска тестов должна быть раскомментированна строка #define TESTING):
main.cpp
C++ (Qt)
#include <stdexcept>
#include <string>
#include <sstream>
#define TESTING
#ifndef TESTING
#include <iostream>
#include <fstream>
class FileError : public std::runtime_error {
public:
FileError( const std::string &fileIn ) : std::runtime_error( "" ), m_file( fileIn ) {
}
virtual const char* what( ) const throw() {
return m_msg.c_str( );
}
std::string getFileName( ) const {
return m_file;
}
virtual ~FileError() throw() {
}
protected:
std::string m_file;
std::string m_msg;
};
class FileOpenError : public FileError {
public:
FileOpenError( const std::string &fileNameIn ) : FileError( fileNameIn ) {
m_msg = "Unable to open " + fileNameIn;
}
};
class FileReadError : public FileError {
public:
FileReadError( const std::string &fileNameIn, int lineNumIn ) :
FileError( fileNameIn ), m_lineNum( lineNumIn ) {
std::ostringstream ostr;
ostr << "Error reading " << fileNameIn << " at line " << lineNumIn;
m_msg = ostr.str( );
}
int getLineNum( ) const {
return m_lineNum;
}
protected:
int m_lineNum;
};
class FileWriteError : public FileError {
public:
FileWriteError( const std::string &fileNameIn ) : FileError( fileNameIn ) {
m_msg = "Unable to write " + fileNameIn;
}
};
#endif // TESTING
class LogicError : public std::logic_error {
public:
LogicError( int argument ) : std::logic_error( "" ), m_argument( argument ) {
}
virtual const char *what( ) const throw () {
return m_message.c_str( );
}
virtual ~LogicError( ) throw () {
}
protected:
int m_argument;
std::string m_message;
};
class InvalidArgument : public LogicError {
public:
InvalidArgument( int argument ) : LogicError( argument ) {
std::stringstream stream;
stream << argument;
m_message = "Argument " + stream.str( ) + " mush be multiple of 5";
}
};
std::string intToString( int number ) {
std::stringstream stream;
stream << number;
return stream.str( );
}
class OutOfRange : public LogicError {
public:
OutOfRange( int argument, int beginOfRange, int endOfRange ) : LogicError( argument ) {
std::string str_argument, str_beginOfRange, str_endOfRange;
str_argument = intToString( argument );
str_beginOfRange = intToString( beginOfRange );
str_endOfRange = intToString( endOfRange );
m_message = "Argument " + str_argument + " don't hit on the range [" +
str_beginOfRange + ", " + str_endOfRange + "]";
}
};
#ifndef TESTING
void readData( const std::string &fileName, int &number )
throw (FileOpenError, FileReadError);
void writeResult( const std::string &fileName, int result )
throw (FileOpenError, FileWriteError);
double fiveAndFive( int number ) throw (InvalidArgument, OutOfRange);
int main() {
std::string fileNameIn = "input.txt";
int number = 0;
try {
readData( fileNameIn, number );
} catch ( const FileError &e ) {
std::cerr << e.what( ) << std::endl;
return 1;
} catch ( ... ) {
std::cerr << "Uncaught exception." << std::endl;
return 1;
}
int result = 0;
try {
result = fiveAndFive(number);
} catch ( const LogicError &e ) {
std::cerr << e.what( ) << std::endl;
return 1;
} catch ( ... ) {
std::cerr << "Uncaught exception." << std::endl;
return 1;
}
std::string fileNameOut = "output.txt";
try {
writeResult( fileNameOut, result );
} catch ( const FileError &e ) {
std::cerr << e.what( ) << std::endl;
return 1;
}
return 0;
}
void readData( const std::string &fileName, int &number )
throw ( FileOpenError, FileReadError ) {
std::ifstream file;
file.open( fileName.c_str( ) );
if ( file.fail( ) ) {
throw FileOpenError( fileName );
}
int lineNumIn = 0;
if ( !( file >> number ) ) {
lineNumIn++;
throw FileReadError( fileName, lineNumIn );
}
}
void writeResult( const std::string &fileName, int result )
throw (FileOpenError, FileWriteError) {
std::ofstream file;
file.open( fileName.c_str( ) );
if ( file.fail( ) ) {
throw FileOpenError( fileName );
}
if ( !( file << result ) ) {
throw FileWriteError( fileName );
}
file << std::endl;
}
#endif // TESTING
double fiveAndFive( int number ) throw (InvalidArgument, OutOfRange) {
if ( (number % 5) != 0 ) {
throw InvalidArgument( number );
}
const int beginOfRange = 5;
const int endOfRange = 400000;
if ( (number < beginOfRange) || (endOfRange < number) ) {
throw OutOfRange( number, beginOfRange, endOfRange );
}
double result = (double) number * (double) number;
return result;
}
Файл проекта:
acmp_0003_five_and_five.pro
C++ (Qt)
SOURCES += \
main.cpp
Заранее спасибо за ответы!
Я на всякий случай, оставлю здесь заметку для тех кто использует C#. В среде Visual Studio Expess 2012 есть полная поддержка методологии Test Driven Development на уровне среды. Удобно создать решение, в котором и создавать тесты. Причём этот фреймворк поддерживает исключения и Mock-объекты. Среда поддерживает чистый TDD: пишем тесты и прямо из тестов генерируем классы и методы, которые будем тестировать (правой кнопкой по названию будущего класса, потом: Generate->Type Name, а для методов тестируемого класса: тоже правой кнопкой -> Generate -> Method Stub). Подробная пошаговая инструкция здесь: http://msdn.microsoft.com/en-us/library/hh212233.aspx
QTest::addColumn<bool>("expect_exception");
Спасибо огромное! Исчезла проблема правки данных в трёх местах :)
Если в коде есть какие-то недочёты, то просьба сообщить:
C++ (Qt)
void FiveAndFive::testCase1_data()
{
QTest::addColumn<int>( "number" );
QTest::addColumn<double>( "expected" );
QTest::addColumn<bool>( "isExpectedException" );
bool exception = true;
bool no_exception = false;
QTest::newRow("fiveAndFive_02") << 0 << 0.0 << exception;
QTest::newRow("fiveAndFive_01") << 1 << 0.0 << exception;
QTest::newRow("fiveAndFive_03") << 4 << 0.0 << exception;
QTest::newRow("fiveAndFive_04") << 5 << 25.0 << no_exception;
QTest::newRow("fiveAndFive_05") << 6 << 0.0 << exception;
QTest::newRow("fiveAndFive_06") << 399995 << 159996000025.0 << no_exception;
QTest::newRow("fiveAndFive_07") << 400000 << 160000000000.0 << no_exception;
QTest::newRow("fiveAndFive_08") << 400001 << 0.0 << exception;
QTest::newRow("fiveAndFive_09") << 400005 << 0.0 << exception;
}
void FiveAndFive::testCase1()
{
QFETCH( int, number );
QFETCH( double, expected );
QFETCH( bool, isExpectedException );
double delta = 0.0001;
try {
double actual = fiveAndFive( number );
bool result = qFuzzyCompare( actual, expected, delta );
if ( isExpectedException ) {
QVERIFY2( false, "There is no exception." );
} else {
QString msg = QString("\nActual: %1"
"\nExpected: %2"
"\nDelta: %3").arg(actual).arg(expected).arg(delta);
QVERIFY2( result, msg.toStdString().c_str() );
}
} catch ( const LogicError& e ) {
if ( !isExpectedException ) {
QVERIFY2( false, "Exception was occur." );
} else {
QVERIFY2( true, "");
}
} catch (...) {
QVERIFY2( false, "Uncaught exception.");
}
}
Я оформил хороший пример по этой теме. См. первое сообщение по следующей ссылке; пример называется "Пример разработки ПО через тестирование" (это в конце инструкции): http://www.prog.org.ru/topic_26413_0.html
Решил проблему. Теперь использую GTest: http://www.prog.org.ru/topic_26944_0.html
При нормальных условиях функция не должна выбрасывать исключения, поэтому всегда в тестах надо использовать ASSERT_NO_THROW:
C++ (Qt)
TEST( test001, normalTest ) {
int input = 15;
int expected = 120;
int actual = 0;
ASSERT_NO_THROW({
actual = myFunc( input );
});
ASSERT_EQ( expected, actual );
}
Если функция при определённых входных данных выбрасывает исключение и вы его ожидаете (например, ваше исключение называется OutOfRange), то надо указать тип исключения:
C++ (Qt)
TEST( test002, exeptionTest ) {
int input = 10001;
ASSERT_THROW({
myFunc( input );
}, OutOfRange);
}
Если вы хотите сравнивать вещественные: ожидаемый результат и выходной результат функции с погрешностью дельта, то надо использовать ASSERT_NEAR:
C++ (Qt)
TEST( test003, normalTest ) {
int input = 5;
double expected = 25.0;
double actual = 0.0;
ASSERT_NO_THROW({
actual = myFunc( n );
});
double delta = 0.001;
ASSERT_NEAR( expected, actual, delta );
}