In recent years, I’ve found myself writing less user interface code and more server code (typically COM servers that are exercised on client workstations or on nodes in a compute farm). Given the lack of a UI for driving and testing the code (excepting spreadsheets), automated tests are vital to achieve a high degree of code coverage.
Given that I’ve been doing this for years now, I was surprised to learn that I haven’t been writing unit tests, as I previously supposed. According to The Art of Unit Testing, the tests I’ve been writing are typically interaction tests. These are useful too, but unless a component can be tested in isolation with tests that run without delay due to network/database interaction, it isn’t a unit test.
Since reading the book, I’ve adopted the Google Test framework for both writing unit tests and implementing mock objects to accompany the tests. Without a good library for writing mock objects, you’re dragged straight back in to writing tests that call other objects in your library. With GMock, you can easily create mock objects and inject behaviour into them in order to test your server code in various scenarios. Using GTest and GMock is easy – just call RUN_ALL_TESTS() in main, and the framework discovers all the unit test you’ve written and launches them:
#include "gmock\gmock.h" using namespace testing; // Start writing unit tests here! int _tmain(int argc, _TCHAR* argv[]) { ::InitGoogleMock( &argc, argv ); return RUN_ALL_TESTS(); }
Here’s a simple example that defines a WeatherStation interface and a UserInterface class. We wish to test the UserInterface, but the weather station isn’t written yet – so we define a MockWeatherStation, and set up tests to invoke the UserInterface in different situations:
#include "gmock\gmock.h" #include <iostream> #include <list> #include <memory> class WeatherStation { public: virtual ~WeatherStation(){}; typedef enum { North, South, East, West } Direction; typedef enum { Optimistic, Pessimistic } Outlook; // NB Semantics on wind deliberately ugly to show a neat feature in gmock virtual void wind( Direction* pDirection, double* strength ) const = 0; virtual double rainfall() const = 0; virtual std::string prediction( Outlook outlook ) const = 0; }; class UserInterface { public: UserInterface( const std::shared_ptr<WeatherStation>& weather_station ) : weather_station_( weather_station ) { } typedef enum { Heavy, Medium, Light } Range; Range rain() { auto rainfall = weather_station_->rainfall(); if ( 0.0 <= rainfall && rainfall < 2.0 ) return Light; else if ( 2.0 <= rainfall && rainfall < 4.0 ) return Medium; else return Heavy; } Range wind() { WeatherStation::Direction direction; double strength; weather_station_->wind( &direction, &strength ); if ( 0.0 <= strength && strength < 5.0 ) return Light; else if ( 5.0 <= strength && strength < 10.0 ) return Medium; else return Heavy; } std::pair<std::string, std::string> predict_range() { return std::make_pair( weather_station_->prediction( WeatherStation::Optimistic ), weather_station_->prediction( WeatherStation::Pessimistic ) ); } private: std::shared_ptr<WeatherStation> weather_station_; }; using namespace testing; class MockWeatherStation : public WeatherStation { public: MOCK_CONST_METHOD0( rainfall, double() ); MOCK_CONST_METHOD2( wind, void(WeatherStation::Direction*, double*) ); MOCK_CONST_METHOD1( prediction, std::string( WeatherStation::Outlook ) ); }; TEST( WeatherStationUserInterface, rain_should_be_heavy ) { auto weather_station = std::make_shared<MockWeatherStation>(); // GMock: specify a simple return value using Return(x) // Here, part of the test is that the Mock should be called once, // hence the 'WillOnce' call (more than one call would be an error). // If multiple calls should happen during the test, // use 'WillRepeatedly' instead. EXPECT_CALL( *weather_station, rainfall() ) .WillOnce( Return(5.0) ); UserInterface ui( weather_station ); EXPECT_EQ( UserInterface::Heavy, ui.rain() ); } TEST( WeatherStationUserInterface, wind_should_be_light ) { auto weather_station = std::make_shared<MockWeatherStation>(); // GMock: specify out parameter values using SetArgPointee EXPECT_CALL( *weather_station, wind(_,_) ) .WillOnce( DoAll( SetArgPointee<0>( WeatherStation::North ), SetArgPointee<1>( 0.5 )) ); UserInterface ui( weather_station ); EXPECT_EQ( UserInterface::Light, ui.wind() ); } TEST( WeatherStationUserInterface, predictions_are_displayed ) { auto weather_station = std::make_shared<MockWeatherStation>(); // GMock: inject more complex logic using C++11 lambdas, // and pattern match on the input value EXPECT_CALL( *weather_station, prediction(WeatherStation::Optimistic) ) .WillOnce( Invoke( []( WeatherStation::Outlook _ ) -> std::string { return "Sunny"; }) ); EXPECT_CALL( *weather_station, prediction(WeatherStation::Pessimistic) ) .WillOnce( Invoke( []( WeatherStation::Outlook _ ) -> std::string { return "Overcast"; }) ); UserInterface ui( weather_station ); auto predicted_range = ui.predict_range(); EXPECT_EQ( "Sunny", predicted_range.first ); EXPECT_EQ( "Overcast", predicted_range.second ); }
When you run the executable, it’s easy to spot if the tests pass or fail:
As a bonus, Visual Studio 2012 has neat integration for GoogleTest:
See also
Pingback: How to Integrate GTest into Visual Studio 2010 | musingstudio
Hi, as you mentioned in this article, “As a bonus, Visual Studio 2012 has neat integration for GoogleTest”, how can I find test cases in Test Explorer window as shown in the snapshot? thanks.
Hi – there’s a Visual Studio extension to install, search for “Google Test Adaptor extension”. After that, tests should appear after building and running the test rig. That’s for VS 2012 – you may be better off now with VS 2013.
OK, it works, thanks.
This was very helpful article. Thanks a lot. The interesting thing for me personally is this:
EXPECT_CALL( *weather_station, rainfall() )
.WillOnce( Return(5.0) );
This means that we do not expect the rainfall() to return 5.0. We set it to return 5.0 when it’s called. This may be confusing for beginners.
Regards.
Hi – glad the article was helpful. I’ve extended the comment on that part of the code to indicate that it is intended to configure what we expect to happen to the mock object during the test. Of course, different tests might require different numbers of calls to the mock object and the mock might return different return values. The beauty of GMock is that we get to re-use the mock object definition for all such tests and the behaviour of the mock during each test is written as part of the test.
Pingback: C++17: if initialiser | musingstudio