diff options
Diffstat (limited to 'kunittest/tester.h')
-rw-r--r-- | kunittest/tester.h | 716 |
1 files changed, 716 insertions, 0 deletions
diff --git a/kunittest/tester.h b/kunittest/tester.h new file mode 100644 index 000000000..f54b85982 --- /dev/null +++ b/kunittest/tester.h @@ -0,0 +1,716 @@ +/* + * tester.h + * + * Copyright (C) 2004 Zack Rusin <zack@kde.org> + * Copyright (C) 2005 Jeroen Wijnhout <Jeroen.Wijnhout@kdemail.net> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef TESTER_H +#define TESTER_H + +/*! @mainpage KUnitTest - a UnitTest library for KDE + * + * @section contents Contents + * @li @ref background + * @li @ref usage + * @li @ref integration + * @li @ref module + * @li @ref advanced + * @li @ref scripts + * + * @section background Background + * + * KUnitTest is based on the "in reality no one wants to write tests and + * if it takes a lot of code no one will. So the less code to write the + * better" design principle. + * + * Copyright and credits: + * @li (C) 2004 Zack Rusin (original author) + * @li Brad Hards (import into CVS) + * @li (C) 2005 Jeroen Wijnhout (GUI, library, module) + * + * You are responsible for what you do with it though. It + * is licensed under a BSD license - read the top of each file. + * + * All the GUI related stuff is in kdesdk/kunittest, the core libraries are in kdelibs/kunittest. + * A simple example modules is in kdelisbs/kunittest/samplemodule.{h,cpp}, however more examples + * can be found in kdesdk/kunittest/example. + * + * There are roughly two ways to use the KUnitTest library. Either you create dynamically + * loadable modules and use the kunittestmodrunner or kunittestguimodrunner programs to run + * the tests, or you use the kunittest/kunittestgui library to create your own test runner + * application. + * + * The main parts of the KUnitTest library are: + * @li runner.{h,cpp} - it is the tester runner, holds all tests and runs + * them. + * @li runnergui.{h,cpp} - the GUI wrapper around the runner. The GUI neatly organizes the + * test results. With the kunittest helper script it can even add the debug output + * to the test results. For this you need to have the kdesdk module installed. + * @li tester.h - which holds the base of a pure test object (Tester). + * @li module.h - defines macros to create a dynamically loadable module. + * + * @section usage Example usage + * + * This section describes how to use the library to create your own tests and runner + * application. + * + * Now lets see how you would add a new test to KUnitTest. You do that by + * writting a Tester derived class which has an "allTests()" method: + * + * @code + * class SampleTest : public Tester + * { + * public: + * SampleTest(); + * + * void allTests(); + * }; + * @endcode + * + * Now in the allTests() method we implement our tests, which might look + * like: + * + * @code + * void SampleTest::allTests() + * { + * CHECK( 3+3, 6 ); + * CHECK( QString( "hello%1" ).arg( " world not" ), QString( "hello world" ) ); + * } + * @endcode + * + * CHECK() is implemented using a template, so you get type safe + * comparison. All that is needed is that the argument types have an + * @c operator==() defined. + * + * Now that you did that the only other thing to do is to tell the + * framework to add this test case, by using the KUNITTEST_REGISTER_TESTER(x) macro. Just + * put the following line in the implementation file: + * + * @code KUNITTEST_REGISTER_TESTER( SampleTest ); @endcode + * + * Note the ;, it is necessary. + * + * KUnitTest will do the rest. It will tell you which tests failed, how, what was the expected + * result, what was the result it got, what was the code that failed and so on. For example for + * the code above it would output: + * + * @verbatim +SampleTest - 1 test passed, 1 test failed + Unexpected failure: + sampletest.cpp[38]: failed on "QString( "hello%1" ).arg( " world not" )" + result = 'hello world not', expected = 'hello world' +@endverbatim + * + * If you use the RunnerGUI class then you will be presented with a scrollable list of the + * test results. + * + * @section integration Integration + * + * The KUnitTest library is easy to use. Let's say that you have the tests written in the + * sampletest.h and sampletest.cpp files. Then all you need is a main.cpp file and a Makefile.am. + * You can copy both from the example file provided with the library. A typical main.cpp file + * looks like this: + * + * @code + * #include <kaboutdata.h> + * #include <kapplication.h> + * #include <kcmdlineargs.h> + * #include <kcmdlineargs.h> + * #include <klocale.h> + * #include <kunittest/runnergui.h> + * + * static const char description[] = I18N_NOOP("SampleTests"); + * static const char version[] = "0.1"; + * static KCmdLineOptions options[] = { KCmdLineLastOption }; + * + * int main( int argc, char** argv ) + * { + * KAboutData about("SampleTests", I18N_NOOP("SampleTests"), version, description, + * KAboutData::License_BSD, "(C) 2005 You!", 0, 0, "mail@provider"); + * + * KCmdLineArgs::init(argc, argv, &about); + * KCmdLineArgs::addCmdLineOptions( options ); + * KApplication app; + * + * KUnitTest::RunnerGUI runner(0); + * runner.show(); + * app.setMainWidget(&runner); + * + * return app.exec(); + * } + * @endcode + * + * The Makefile.am file will look like: + * + * @code + * INCLUDES = -I$(top_srcdir)/src $(all_includes) + * METASOURCES = AUTO + * check_PROGRAMS = sampletests + * sampletests_SOURCES = main.cpp sampletest.cpp + * sampletests_LDFLAGS = $(KDE_RPATH) $(all_libraries) + * sampletests_LDADD = -lkunittest + * noinst_HEADERS = sampletest.h + * + * check: + * kunittest $(top_builddir)/src/sampletests SampleTests + * @endcode + * + * Most of this Makefile.am will be self-explanatory. After running + * "make check" the binary "sampletests" will be built. The reason for + * adding the extra make target "check" is that you probably do not want + * to rebuild the test suite everytime you run make. + * + * You can run the binary on its own, but you get more functionality if you use + * the kunittest helper script. The Makefile.am is set up in such + * a way that this helper script is automatically run after you do a + * "make check". This scripts take two arguments, the first is the path + * to the binary to run. The second the application name, in this case SampleTests. + * This name is important since it is used to let the script communicate with the application + * via DCOP. The helper scripts relies on the Perl DCOP bindings, so these need to be installed. + * + * @section module Creating test modules + * + * If you think that writing your own test runner if too much work then you can also + * use the kunittestermodrunner application or the kunitguimodrunner script to run + * the tests for you. You do have to put your tests in a dynamically loadable module though. + * Fortunately KUnitTest comes with a few macros to help you do this. + * + * First the good news, you don't have to change the header file sampletest.h. However, we + * will rename it to samplemodule.h, so we remember we are making a module. The + * implementation file should be rename to samplemodule.cpp. This file requires some + * modifications. First we need to include the module.h header: + * + * @code + * #include <kunittest/module.h> + * @endcode + * + * This header file is needed because it defines some macro you'll need. In fact this is + * how you use them: + * + * @code + * KUNITTEST_MODULE( kunittest_samplemodule, "Tests for sample module" ); + * KUNITTEST_MODULE_REGISTER_TESTER( SimpleSampleTester ); + * KUNITTEST_MODULE_REGISTER_TESTER( SomeSampleTester ); + * @endcode + * + * The first macro, KUNITTEST_MODULE(), makes sure that the module can be loaded and that + * the test classes are created. The first argument "kunittest_samplemodule" is the library + * name, in this case the library we're creating a kunittest_samplemodule.la module. The + * second argument is name which will appear in the test runner for this test suite. + * + * The tester class are now added by the KUNITTEST_MODULE_REGISTER_TESTER() macro, not the + * KUNITTEST_REGISTER_TESTER(). The only difference between the two is that you have to + * pass the module class name to this macro. + * + * The Makefile.am is also a bit different, but not much: + * + * @code + * INCLUDES = -I$(top_srcdir)/include $(all_includes) + * METASOURCES = AUTO + * check_LTLIBRARIES = kunittest_samplemodule.la + * kunittest_samplemodule_la_SOURCES = samplemodule.cpp + * kunittest_samplemodule_la_LIBADD = $(LIB_KUNITTEST) + * kunittest_samplemodule_la_LDFLAGS = -module $(KDE_CHECK_PLUGIN) $(all_libraries) + * @endcode + * + * The $(KDE_CHECK_PLUGIN) macro is there to make sure a dynamically loadable + * module is created. + * + * After you have built the module you open a Konsole and cd into the build folder. Running + * the tests in the module is now as easy as: + * + * @code + * $ make check && kunittestmodrunner + * @endcode + * + * The kunittestmodrunner application loads all kunittest_*.la modules in the current + * directory. The exit code of this console application is the number of unexpected failures. + * + * If you want the GUI, you should use the kunittestmod script: + * + * @code + * $ make check && kunittestmod + * @endcode + * + * This script starts kunittestguimodrunner application and a helper script to take + * care of dealing with debug output. + * + * @section advanced Advanced usage + * + * Normally you just want to use CHECK(). If you are developing some more + * tests, and they are run (or not) based on some external dependency, + * you may need to skip some tests. In this case, rather than doing + * nothing (or worse, writing a test step that aborts the test run), you + * might want to use SKIP() to record that. Note that this is just a + * logging / reporting tool, so you just pass in a string: + * + * @code + * SKIP( "Test skipped because of lack of foo support." ); + * @endcode + * + * Similarly, you may have a test step that you know will fail, but you + * don't want to delete the test step (because it is showing a bug), but + * equally you can't fix it right now (eg it would break binary + * compatibility, or would violate a string freeze). In that case, it + * might help to use XFAIL(), for "expected failure". The test will still + * be run, and recorded as a failure (assuming it does fail), but will + * also be recorded separately. Usage might be as follows: + * + * @code + * XFAIL( 2+1, 4 ); + * @endcode + * + * You can mix CHECK(), SKIP() and XFAIL() within a single Tester derived + * class. + * + * + * @section exceptions Exceptions + * + * KUnitTest comes with simple support for testing whether an exception, such as a function call, + * throws an exception or not. Simply, for the usual macros there corresponding ones for + * exception testing: CHECK_EXCEPTION(), XFAIL_EXCEPTION(), and SKIP_EXCEPTION(). They all take two + * arguments: the expression that will catch the exception, and the expression that is supposed + * to throw the exception. + * + * For example: + * + * @code + * CHECK_EXCEPTION(EvilToothFairyException *, myFunction("I forgot to brush my teeth!")); + * @endcode + * + * @note The exception is not de-allocated in anyway. + * + * The macros does not allow introspection of the exceptions, such as testing a supplied + * identifier code on the exception object or similar; this requires manual coding, such + * as custom macros. + * + * @section scripts Scripts + * + * The library comes with several helper scripts: + * + * @li kunittest [app] [dcopobject] : Runs the application app and redirects all debug output to the dcopobject. + * @li kunittestmod --folder [folder] --query [query] : Loads and runs all modules in the folder matching the query. Use a GUI. + * @li kunittest_debughelper [dcopobject] : A PERL script that is able to redirect debug output to a RunnerGUI instance. + * + * These scripts are part of the kdesdk/kunittest module. + */ + +/*! + * @file tester.h + * Defines macros for unit testing as well as some test classes. + */ + +#include <iostream> +using namespace std; + +#include <qobject.h> +#include <qstringlist.h> +#include <qasciidict.h> + +#include <kdelibs_export.h> + +/*! @def CHECK(x,y) + * Use this macro to perform an equality check. For example + * + * @code CHECK( numberOfErrors(), 0 ); @endcode + */ +#define CHECK( x, y ) check( __FILE__, __LINE__, #x, x, y, false ) + +/// for source-compat with qttestlib: use COMPARE(x,y) if you plan to port to qttestlib later. +#define COMPARE CHECK + +/// for source-compat with qttestlib: use VERIFY(x) if you plan to port to qttestlib later. +#define VERIFY( x ) CHECK( x, true ) + +/*! @def XFAIL(x,y) + * Use this macro to perform a check you expect to fail. For example + * + * @code XFAIL( numberOfErrors(), 1 ); @endcode + * + * If the test fails, it will be counted as such, however it will + * also be registered separately. + */ +#define XFAIL( x, y ) check( __FILE__, __LINE__, #x, x, y, true ) + +/*! @def SKIP(x) + * Use this macro to indicate that a test is skipped. + * + * @code SKIP("Test skipped because of lack of foo support."); @endcode + */ +#define SKIP( x ) skip( __FILE__, __LINE__, QString::fromLatin1(#x)) + +/*! + * A macro testing that @p expression throws an exception that is catched + * with @p exceptionCatch. Use it to test that an expression, such as a function call, + * throws a certain exception. + * + * @note this macro assumes it's used in a function which is a sub-class of the Tester class. + */ +#define CHECK_EXCEPTION(exceptionCatch, expression) \ + try \ + { \ + expression; \ + } \ + catch(exceptionCatch) \ + { \ + setExceptionRaised(true); \ + } \ + if(exceptionRaised()) \ + { \ + success(QString(__FILE__) + "[" + QString::number(__LINE__) + "]: passed " + #expression); \ + } \ + else \ + { \ + failure(QString(__FILE__) + "[" + QString::number(__LINE__) + QString("]: failed to throw " \ + "an exception on: ") + #expression); \ + } \ + setExceptionRaised(false); + +/*! + * This macro is similar to XFAIL, but is for exceptions instead. Flags @p expression + * as being expected to fail to throw an exception that @p exceptionCatch is supposed to catch. + */ +#define XFAIL_EXCEPTION(exceptionCatch, expression) \ + try \ + { \ + expression; \ + } \ + catch(exceptionCatch) \ + { \ + setExceptionRaised(true); \ + } \ + if(exceptionRaised()) \ + { \ + unexpectedSuccess(QString(__FILE__) + "[" + QString::number(__LINE__) + "]: unexpectedly threw an exception and passed: " + #expression); \ + }\ + else \ + { \ + expectedFailure(QString(__FILE__) + "[" + QString::number(__LINE__) + QString("]: failed to throw an exception on: ") + #expression); \ + } \ + setExceptionRaised(false); + +/*! + * This macro is similar to SKIP, but is for exceptions instead. Skip testing @p expression + * and the @p exceptionCatch which is supposed to catch the exception, and register the test + * as being skipped. + */ +#define SKIP_EXCEPTION(exceptionCatch, expression) \ + skip( __FILE__, __LINE__, QString("Exception catch: ")\ + .arg(QString(#exceptionCatch)).arg(QString(" Test expression: ")).arg(QString(#expression))) + +/** + * Namespace for Unit testing classes + */ +namespace KUnitTest +{ + /*! A simple class that encapsulates a test result. A Tester class usually + * has a single TestResults instance associated with it, however the SlotTester + * class can have more TestResults instances (one for each test slot in fact). + */ + class KUNITTEST_EXPORT TestResults + { + friend class Tester; + + public: + TestResults() : m_tests( 0 ) {} + + virtual ~TestResults() {} + + /*! Clears the test results and debug info. Normally you do not need to call this. + */ + virtual void clear() + { + m_errorList.clear(); + m_xfailList.clear(); + m_xpassList.clear(); + m_skipList.clear(); + m_successList.clear(); + m_debug = ""; + m_tests = 0; + } + + /*! Add some debug info that can be view later. Normally you do not need to call this. + * @param debug The debug info. + */ + virtual void addDebugInfo(const QString &debug) + { + m_debug += debug; + } + + /*! @returns The debug info that was added to this Tester object. + */ + QString debugInfo() const { return m_debug; } + + /*! @returns The number of finished tests. */ + int testsFinished() const { return m_tests; } + + /*! @returns The number of failed tests. */ + int errors() const { return m_errorList.count(); } + + /*! @returns The number of expected failures. */ + int xfails() const { return m_xfailList.count(); } + + /*! @returns The number of unexpected successes. */ + int xpasses() const { return m_xpassList.count(); } + + /*! @returns The number of skipped tests. */ + int skipped() const { return m_skipList.count(); } + + /*! @returns The number of passed tests. */ + int passed() const { return m_successList.count(); } + + /*! @returns Details about the failed tests. */ + QStringList errorList() const { return m_errorList; } + + /*! @returns Details about tests that failed expectedly. */ + QStringList xfailList() const { return m_xfailList; } + + /*! @returns Details about tests that succeeded unexpectedly. */ + QStringList xpassList() const { return m_xpassList; } + + /*! @returns Details about which tests were skipped. */ + QStringList skipList() const { return m_skipList; } + + /*! @returns Details about the succeeded tests. */ + QStringList successList() const { return m_successList; } + + private: + QStringList m_errorList; + QStringList m_xfailList; + QStringList m_xpassList; + QStringList m_skipList; + QStringList m_successList; + QString m_debug; + int m_tests; + }; + + typedef QAsciiDict<TestResults> TestResultsListType; + + /*! A type that can be used to iterate through the registry. */ + typedef QAsciiDictIterator<TestResults> TestResultsListIteratorType; + + /*! The abstract Tester class forms the base class for all test cases. Users must + * implement the void Tester::allTests() method. This method contains the actual test. + * + * Use the CHECK(x,y), XFAIL(x,y) and SKIP(x) macros in the allTests() method + * to perform the tests. + * + * @see CHECK, XFAIL, SKIP + */ + class KUNITTEST_EXPORT Tester : public QObject + { + public: + Tester(const char *name = 0L) + : QObject(0L, name), m_results(new TestResults()), m_exceptionState(false) + {} + + virtual ~Tester() { delete m_results; } + + public: + /*! Implement this method with the tests and checks you want to perform. + */ + virtual void allTests() = 0; + + public: + /*! @return The TestResults instance. + */ + virtual TestResults *results() { return m_results; } + + protected: + /*! This is called when the SKIP(x) macro is used. + * @param file A C-string containing the name of the file where the skipped tests resides. Typically the __FILE__ macro is used to retrieve the filename. + * @param line The linenumber in the file @p file. Use the __LINE__ macro for this. + * @param msg The message that identifies the skipped test. + */ + void skip( const char *file, int line, QString msg ) + { + QString skipEntry; + QTextStream ts( &skipEntry, IO_WriteOnly ); + ts << file << "["<< line <<"]: " << msg; + skipTest( skipEntry ); + } + + /*! This is called when the CHECK or XFAIL macro is used. + * @param file A C-string containing the name of the file where the skipped tests resides. Typically the __FILE__ macro is used to retrieve the filename. + * @param line The linenumber in the file @p file. Use the __LINE__ macro for this. + * @param str The message that identifies the skipped test. + * @param result The result of the test. + * @param expectedResult The expected result. + * @param expectedFail Indicates whether or not a failure is expected. + */ + template<typename T> + void check( const char *file, int line, const char *str, + const T &result, const T &expectedResult, + bool expectedFail ) + { + cout << "check: " << file << "["<< line <<"]" << endl; + + if ( result != expectedResult ) + { + QString error; + QTextStream ts( &error, IO_WriteOnly ); + ts << file << "["<< line <<"]: failed on \"" << str + <<"\" result = '" << result << "' expected = '" << expectedResult << "'"; + + if ( expectedFail ) + expectedFailure( error ); + else + failure( error ); + + } + else + { + // then the test passed, but we want to record it if + // we were expecting a failure + if (expectedFail) + { + QString err; + QTextStream ts( &err, IO_WriteOnly ); + ts << file << "["<< line <<"]: " + <<" unexpectedly passed on \"" + << str <<"\""; + unexpectedSuccess( err ); + } + else + { + QString succ; + QTextStream ts( &succ, IO_WriteOnly ); + ts << file << "["<< line <<"]: " + <<" passed \"" + << str <<"\""; + success( succ ); + } + } + + ++m_results->m_tests; + } + + /*! + * This function can be used to flag succeeding tests, when + * doing customized tests while not using the check function. + * + * @param message the message describing what failed. Should be informative, + * such as mentioning the expression that failed and where, the file and file number. + */ + void success(const QString &message) { m_results->m_successList.append(message); } + + /*! + * This function can be used to flag failing tests, when + * doing customized tests while not using the check function. + * + * @param message the message describing what failed. Should be informative, + * such as mentioning the expression that failed and where, the file name and file number. + */ + void failure(const QString &message) { m_results->m_errorList.append(message); } + + /*! + * This function can be used to flag expected failures, when + * doing customized tests while not using the check function. + * + * @param message the message describing what failed. Should be informative, + * such as mentioning the expression that failed and where, the file name and file number. + */ + void expectedFailure(const QString &message) { m_results->m_xfailList.append(message); } + + /*! + * This function can be used to flag unexpected successes, when + * doing customized tests while not using the check function. + * + * @param message the message describing what failed. Should be informative, + * such as mentioning the expression that failed and where, the file name and file number. + */ + void unexpectedSuccess(const QString &message) { m_results->m_xpassList.append(message); } + + /*! + * This function can be used to flag a test as skipped, when + * doing customized tests while not using the check function. + * + * @param message the message describing what failed. Should be informative, + * such as mentioning the expression that failed and where, the file name and file number. + */ + void skipTest(const QString &message) { m_results->m_skipList.append(message); } + + /*! + * exceptionRaised and exceptionState are book-keeping functions for testing for + * exceptions. setExceptionRaised sets an internal boolean to true. + * + * @see exceptionRaised + * @param state the new + */ + void setExceptionRaised(bool state) { m_exceptionState = state; } + + /*! + * Returns what the currently tested exception state. + * + * @see setExceptionRaised + */ + bool exceptionRaised() const + { + return m_exceptionState; + } + + protected: + TestResults *m_results; + + private: + + bool m_exceptionState; + }; + + /*! The SlotTester class is a special Tester class, one that will + * execute all slots that start with the string "test". The method + * void allTests() is implemented and should not be overriden. + */ + class KUNITTEST_EXPORT SlotTester : public Tester + { + Q_OBJECT + + public: + SlotTester(const char *name = 0L); + + void allTests(); + + TestResults *results(const char *sl); + + TestResultsListType &resultsList() { return m_resultsList; } + + signals: + void invoke(); + + private: + void invokeMember(const QString &str); + + TestResultsListType m_resultsList; + TestResults *m_total; + }; +} + +KUNITTEST_EXPORT QTextStream& operator<<( QTextStream& str, const QRect& r ); + +KUNITTEST_EXPORT QTextStream& operator<<( QTextStream& str, const QPoint& r ); + +KUNITTEST_EXPORT QTextStream& operator<<( QTextStream& str, const QSize& r ); + +#endif |