Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Professional C++ [eng].pdf
Скачиваний:
284
Добавлен:
16.08.2013
Размер:
11.09 Mб
Скачать

Becoming Adept at Testing

As you will see in the example below, unit tests are usually very small and simple pieces of code. In most cases, writing a single unit test will only take a few minutes, making them one of the most productive uses of your time.

Run the Tests

When you’re done writing a test, you should run it right away before the anticipation of the results becomes too much to bear. The joy of a screen full of passing unit tests shouldn’t be minimized. For most programmers, this is the easiest way to see quantitative data that declare your code useful and correct.

Even if you adopt the methodology of writing tests prior to writing code, you should still run the tests immediately after they are written. This way, you can prove to yourself that the test fails initially. Once the code is in place, you have tangible data that shows that it accomplished what it was supposed to accomplish.

It’s unlikely that every test you write will have the expected result the first time. In theory, if you are writing tests before writing code, all of your tests should fail. If one passes, either the code magically appeared or there is a problem with a test. If the code is done and tests still fail (some would say that if the tests fail, the code is actually not done!), there are two possibilities. The code could be wrong or the test could be wrong. As mentioned previously, it’s often quite tempting to turn to the test and twiddle some Booleans to make everything work. Resist this urge!

Unit Testing in Action

Now that you’ve read about unit testing in theory, it’s time to actually write some tests. The following example draws on the object pool example from Chapter 17. As a brief recap, the object pool is a class that can be used to avoid excessive object creation. By keeping track of already created objects, the pool acts as a broker between code that needs a certain type of object and such objects that already exist.

The public interface for ObjectPool is shown here:

//

//template class ObjectPool

//Provides an object pool that can be used with any class that provides a

//default constructor.

//

//The object pool constructor creates a pool of objects, which it hands out

//to clients when requested via the acquireObject() method. When a client is

//finished with the object it calls releaseObject() to put the object back

//into the object pool.

//

//The constructor and destructor on each object in the pool will be called only

//once each for the lifetime of the program, not once per acquisition and release.

//The primary use of an object pool is to avoid creating and deleting objects

//repeatedly. The object pool is most suited to applications that use large

//numbers of objects for short periods of time.

//

//For efficiency, the object pool doesn’t perform sanity checks.

//Expects the user to release every acquired object exactly once.

//Expects the user to avoid using any objects that he or she has released.

515

Chapter 19

//Expects the user not to delete the object pool until every object

//that was acquired has been released. Deleting the object pool invalidates

//any objects that the user had acquired, even if they had not yet been released.

template <typename T> class ObjectPool

{

public:

//Creates an object pool with chunkSize objects.

//Whenever the object pool runs out of objects, chunkSize

//more objects will be added to the pool. The pool only grows:

//objects are never removed from the pool (freed), until

//the pool is destroyed.

//

// Throws invalid_argument if chunkSize is <= 0.

//

ObjectPool(int chunkSize = kDefaultChunkSize) throw(std::invalid_argument, std::bad_alloc);

//

//Frees all the allocated objects. Invalidates any objects that have

//been acquired for use.

//

~ObjectPool();

//

//Reserve an object for use. The reference to the object is invalidated

//if the object pool itself is freed.

//

// Clients must not free the object!

//

T& acquireObject();

//

//Return the object to the pool. Clients must not use the object after

//it has been returned to the pool.

//

void releaseObject(T& obj);

// [Private/Protected methods and data omitted]

};

If the notion of an object pool is new to you, you may wish to peruse Chapter 17 before continuing with this example.

Introducing cppunit

cppunit is an open-source unit testing framework for C++ that is based on a Java package called junit. The framework is fairly lightweight (in a good way), and it is very easy to get started. The advantage of using a framework such as cppunit is that it allows the developer to focus on writing tests instead of dealing with setting up tests, building logic around tests, and gathering results. cppunit includes a number of helpful utilities for test developers and automatic output in various formats. The full breadth of features is not covered here. We suggest you read up on cppunit at http://cppunit.sourceforge.net.

516

Becoming Adept at Testing

The most common way of using cppunit is to subclass the CppUnit::TestFixture class (note that CppUnit is the namespace and TestFixture is the class). A fixture is simply a logical group of tests. A TestFixture subclass can override the setUp() method to perform any tasks that need to happen prior to the tests running as well as the tearDown() method, which can be used to clean up after the tests have run. A fixture can also maintain state with member variables. A skeleton implementation of ObjectPoolTest, a class for testing the ObjectPool class, is shown here:

// ObjectPoolTest.h

#include <cppunit/TestFixture.h>

class ObjectPoolTest : public CppUnit::TestFixture

{

public:

void setUp(); void tearDown();

};

Because the tests for ObjectPool are relatively simple and isolated, empty definitions will suffice for setUp() and tearDown(). The beginning stage of the source file is shown here:

// ObjectPoolTest.cpp

#include “ObjectPoolTest.h”

void ObjectPoolTest::setUp()

{

}

void ObjectPoolTest::tearDown()

{

}

That’s all the initial code we need to start developing unit tests!

Writing the First Test

Since this may be your first exposure to cppunit, or to unit tests at large, the first test will be a very simple one. It simply tests whether 0 < 1.

An individual unit test in cppunit is just a method of the fixture class. To create a simple test, add its declaration to the ObjectPoolTest.h file:

// ObjectPoolTest.h

#include <cppunit/TestFixture.h>

class ObjectPoolTest : public CppUnit::TestFixture

{

public:

void setUp(); void tearDown();

517

Chapter 19

// Our first test!

void testSimple();

};

The test definition uses the CPPUNIT_ASSERT macro to perform the actual test. CPPUNIT_ASSERT, like other assert macros you may have used, simply surrounds an expression that should be true. See Chapter 20 for details on assert. In this case, the test claims that 0 is less than 1, so it surrounds the statement 0 < 1 in a CPPUNIT_ASSERT macro call. This macro is defined in the cppunit/TestAssert.h file.

// ObjectPoolTest.cpp

#include “ObjectPoolTest.h” #include <cppunit/TestAssert.h>

void ObjectPoolTest::setUp()

{

}

void ObjectPoolTest::tearDown()

{

}

void ObjectPoolTest::testSimple()

{

CPPUNIT_ASSERT(0 < 1);

}

That’s it! Of course, most of your unit tests will do something a bit more interesting than a simple assert. As you will see, the common pattern is to perform some sort of calculation and assert that the result is the value you expected. With cppunit, you don’t even need to worry about exceptions — the framework will catch and report them as necessary.

Building a Suite of Tests

There are a few more steps before the simple test can be run. cppunit runs a group of tests as a suite. A suite tells cppunit which tests to run, as opposed to a fixture, which simply groups tests together logically. The common pattern is to give your fixture class a static method that builds a suite containing all of its tests. In the updated versions of ObjectPoolTest.h and ObjectPoolTest.cpp, the suite() method is used for this purpose.

// ObjectPoolTest.h

#include <cppunit/TestFixture.h> #include <cppunit/TestSuite.h>

#include <cppunit/Test.h>

class ObjectPoolTest : public CppUnit::TestFixture

{

public:

void setUp(); void tearDown();

// Our first test! void testSimple();

518

Becoming Adept at Testing

static CppUnit::Test* suite();

};

// ObjectPoolTest.cpp

#include “ObjectPoolTest.h” #include <cppunit/TestAssert.h>

void ObjectPoolTest::setUp()

{

}

void ObjectPoolTest::tearDown()

{

}

void ObjectPoolTest::testSimple()

{

CPPUNIT_ASSERT(0 < 1);

}

CppUnit::Test* ObjectPoolTest::suite()

CppUnit::TestSuite* suiteOfTests = new CppUnit::TestSuite(“ObjectPoolTest”); suiteOfTests->addTest(new CppUnit::TestCaller<ObjectPoolTest>(

“testSimple”, &ObjectPoolTest::testSimple ) );

return suiteOfTests; // Note that Test is a superclass of TestSuite

}

The template syntax for creating a TestCaller is a bit dense, but just about every single test you write will follow this exact pattern, so you can ignore the implementation of TestSuite and TestCaller for the most part.

To actually run the suite of tests and see the results, you will need a test runner. cppunit is a flexible framework. It contains several different runners that operate in different environments, such as the MFC Runner, which is designed to run within a program written with the Microsoft Foundation Classes. For a text-based environment, you should use the Text Runner, which is defined in the CppUnit::TextUi namespace.

The code to run the suite of tests defined by the ObjectPoolTest fixture follows. It simply creates a runner, adds the tests returned by the suite() method, and calls run().

// main.cpp

#include “ObjectPoolTest.h”

#include <cppunit/ui/text/TestRunner.h>

int main(int argc, char** argv)

{

CppUnit::TextUi::TestRunner runner; runner.addTest(ObjectPoolTest::suite());

519

Chapter 19

runner.run();

}

After the code is all compiled, linked, and run, you should see output similar to the following:

OK (1 tests)

If you modify the code to assert that 1 < 0, the test will fail, cppunit will report the failure as follows:

!!!FAILURES!!!

Test Results:

Run: 1 Failures: 1 Errors: 0

1) test: testSimple (F) line: 21 ObjectPoolTest.cpp assertion failed

- Expression: 1 < 0

Note that by using the CPPUNIT_ASSERT macro, the framework was able to pinpoint the exact line on which the test failed — a useful piece of information for debugging!

Adding the Real Tests

Now that the framework is all set up and a simple test is working, it’s time to turn your attention to the ObjectPool class and write some code that actually tests it. All of the following tests will be added to

ObjectPoolTest.h and ObjectPoolTest.cpp, just like the earlier simple test.

Before you can write the tests, you’ll need a helper object to use with the ObjectPool class. As you recall, the ObjectPool creates chunks of objects of a certain type and hands them out to the caller as requested. Some of the tests will need to check if a retrieved object is the same as a previously retrieved object. One way to do this is to create a pool of serial objects — objects that have a monotonically increasing serial number. The following code defines such a class:

// Serial.h

class Serial

{

public:

Serial();

int getSerialNumber() const;

protected:

static int sNextSerial;

int mSerialNumber;

};

// Serial.cpp

#include “Serial.h”

520

Becoming Adept at Testing

Serial::Serial()

{

// A new object gets the next serial number. mSerialNumber = sNextSerial++;

}

int Serial::getSerialNumber() const

{

return mSerialNumber;

}

int Serial::sNextSerial = 0; // The first serial number is 0.

On to the tests! As an initial sanity check, you might want a test that simply creates an object pool. If any exceptions are thrown during creation, cppunit will report an error:

void ObjectPoolTest::testCreation()

{

ObjectPool<Serial> myPool;

}

The next test is a negative test because it is doing something that should fail. In this case, the test tries to create an object pool with an invalid chunk size of 0. The object pool constructor should throw an exception. Normally, cppunit would catch the exception and report an error. However, since that is the desired behavior, the test catches the exception explicitly and sets a flag. The final step of the test is to assert that the flag was set. Thus, if the constructor does not throw an exception, the test will fail:

void ObjectPoolTest::testInvalidChunkSize()

{

bool caughtException = false;

try {

ObjectPool<Serial> myPool(0);

}catch (const invalid_argument& ex) {

//OK. We were expecting an exception.

caughtException = true;

}

CPPUNIT_ASSERT(caughtException);

}

testAcquire() tests a specific piece of public functionality — the ability of the ObjectPool to give out an object. In this case, there is not much to assert. To prove validity of the resulting Serial reference, the test asserts that its serial number is greater than or equal to zero.

void ObjectPoolTest::testAcquire()

{

ObjectPool<Serial> myPool;

Serial& serial = myPool.acquireObject();

CPPUNIT_ASSERT(serial.getSerialNumber() >= 0);

}

521

Chapter 19

The next test is a bit more interesting. The ObjectPool should not give out the same Serial object twice (unless it is explicitly released). This test checks the exclusivity property of the ObjectPool by creating a pool with a fixed chunk size and retrieving exactly that many objects. If the pool is properly dishing out unique objects, none of their serial numbers should match. Note that this test only covers objects created as part of a single chunk. A similar test for multiple chunks would be an excellent idea.

void ObjectPoolTest::testExclusivity()

{

const int poolSize = 5; ObjectPool<Serial> myPool(poolSize); set<int> seenSerials;

for (int i = 0; i < poolSize; i++) {

Serial& nextSerial = myPool.acquireObject();

// Assert that this number hasn’t been seen before. CPPUNIT_ASSERT(seenSerials.find(nextSerial.getSerialNumber()) ==

seenSerials.end());

// Add this number to the set. seenSerials.insert(nextSerial.getSerialNumber());

}

}

This implementation uses the set container from the STL. Consult Chapter 21 for details if you are unfamiliar with this container.

The final test (for now) checks the release functionality. Once an object is released, the ObjectPool can give it out again. The pool shouldn’t create additional chunks until it has recycled all released objects. This test first retrieves a Serial from the pool and records its serial number. Then, the object is immediately released back into the pool. Next, objects are retrieved from the pool until either the original object is recycled (identified by its serial number) or the chunk has been used up. If the code gets all the way through the chunk without seeing the recycled object, the test fails.

void ObjectPoolTest::testRelease()

{

const int poolSize = 5; ObjectPool<Serial> myPool(poolSize);

Serial& originalSerial = myPool.acquireObject();

int originalSerialNumber = originalSerial.getSerialNumber();

//Return the original object to the pool. myPool.releaseObject(originalSerial);

//Now make sure that the original object is recycled before

//a new chunk is created.

bool wasRecycled = false;

for (int i = 0; i < poolSize; i++) {

Serial& nextSerial = myPool.acquireObject();

if (nextSerial.getSerialNumber() == originalSerialNumber) { wasRecycled = true;

break;

522