Boost: How do I write a unit test for a signal?

Today, while writing some unit tests, I encountered a challenge. The user story was that, when a Person’s details are updated, the display should be updated to reflect the changes.

I’d implemented this feature using a signal on the person class that will be called whenever any details are updated:

class person
{
public:
	// Set the Person's name.
	void name(const std::string & name)
	{
		name_ = name;
		updated(*this);
	};
	
	// An signal that will be called when the person's details are updated.
	boost::signal<void(const person & person)> updated;

private:
	// The person's name.
	std::string name_;
};

This is a fairly standard application of an observer pattern that you might find in any MVC application.

But the question is, using the Boost unit test framework, how can I test if my signal has been called?

The mock signal handler

To test the signal handler, we’ll use a functor as a mock signal handler, that sets an internal flag when it gets called. In the functor’s destructor, we’ll do a test on the flag to make sure it got set:

struct mock_handler
{
	mock_handler(const person & expected_person) : 
		has_been_called_(false), expected_person_(expected_person) {};
	
	// The signal handler function.
	void operator()(const person & person)
	{
		has_been_called_ = true;
		BOOST_CHECK_EQUAL(&person == &expected_person_, true);
	};
	
	// This handler must be called before it goes out of scope.
	~mock_handler()
	{
		BOOST_CHECK_EQUAL(has_been_called_, true);
	};

private:
	bool has_been_called_;
	const person & expected_person_;
};

The test case

Once we’ve written a mock, the test case is pretty simple. Note that I wrap my handler with a boost::ref, so that it doesn’t get copied.

// Test that setting a new name triggers the person.updated signal.
BOOST_AUTO_TEST_CASE(setting_name_triggers_update_signal)
{
	person subject;
	mock_handler handler(subject);
	
	subject.updated.connect(boost::ref(handler));
	
	// Change the person's name, triggering the updated signal.
	subject.name("Richard");
}

This works great. And if we comment out the updated signal call in person::name():

Running 1 test case... person_test.cpp(49): error in "setting_name_triggers_update_signal": check has_been_called_ == true failed [0 != 1]

*** 1 failure detected in test suite "tests"

..then the test case will fail accordingly.

June 8, 2008

3 Comments

sheepsimulator on April 28, 2010 at 9:42 pm.

Brilliant! This is a great article for people who are learning how to do unit tests! Thanks!

sheepsimulator on May 15, 2010 at 9:48 pm.

Note: if you use this idiom to write a checker method, be careful about putting a unit-test-system assert inside the destructor of a mock class if it throws a C++ exception. If the assert fails in operator() *and* the assert fails in the destructor, you’ll only see the destructor’s assert and not the one from operator().

sheepsimulator on June 21, 2010 at 2:43 am.

One of my coworkers made the following suggestion in code review using this method. As written, the handler tests your code as a _side effect_, which assumes you know what handler is doing. It might be better to be explicit in your tests about what you expect to happen in the handler. You could do this:

struct mock_handler
{
mock_handler() :
has_been_called_(false)
{};

// The signal handler function.
void operator()(const person & person)
{
has_been_called_ = true;
received_person_ = person;
};

bool has_been_called()
{
return has_been_called_;
}

bool person_received()
{
return actual_person;
}

// This handler must be called before it goes out of scope.
~mock_handler()
{
};

private:
bool has_been_called_;
person& received_person_;
};

And then have:

// Test that setting a new name triggers the person.updated signal.
BOOST_AUTO_TEST_CASE(setting_name_triggers_update_signal)
{
person subject;
mock_handler handler(subject);

subject.updated.connect(boost::ref(handler));

// Change the person’s name, triggering the updated signal.
subject.name(“Richard”);

BOOST_CHECK_EQUAL(handler.has_been_called(), true);
BOOST_CHECK_EQUAL(handler.person_received(), subject);
}

This makes it more explicit as to what the handler is expecting to get, and may be easier to read/maintain.

Leave a Reply