560 likes | 594 Views
Stopping the rot. Putting legacy C++ under test Seb Rose ACCU 2011. Agenda. Background Unit Testing Frameworks Test & Mock: First Examples Refactoring: Wrap Dependency Refactoring: Extract Component Refactoring: Non-Intrusive C Seam Conclusions Questions. Agenda. Background
E N D
Stopping the rot Putting legacy C++ under test Seb Rose ACCU 2011
Agenda • Background • Unit Testing • Frameworks • Test & Mock: First Examples • Refactoring: Wrap Dependency • Refactoring: Extract Component • Refactoring: Non-Intrusive C Seam • Conclusions • Questions
Agenda • Background • Unit Testing • Frameworks • Test & Mock: First Examples • Refactoring: Wrap Dependency • Refactoring: Extract Component • Refactoring: Non-Intrusive C Seam • Conclusions • Questions
A brief history of DOORS • Developed in C in early 1990s • Home grown cross platform GUI • Heavy use of pre-processor macros • Server and client share codebase • Ported to C++ in 1999 • No unit tests – ever • DXL extension language tests brittle • Success led to rapid team growth • Proliferation of products and integrations
Challenges • Highly coupled code • Long build times • Developer ‘silos’ • SRD - “Big Design Up Front” • Long manual regression test ‘tail’ • Hard to make modifications without errors • No experience writing unit tests
New direction • Move to iterative development • Implementation driven by User Stories not SRD • All new/modified code to have unit tests • All unit tests to be run every build • Nightly builds • CI server • Develop “Whole Team” approach • Automated acceptance tests written by test & dev • Test to pick up nightly builds • Align with Rational toolset
Agenda • Background • Unit Testing • Frameworks • Test & Mock: First Examples • Refactoring: Wrap Dependency • Refactoring: Extract Component • Refactoring: Non-Intrusive C Seam • Conclusions • Questions
Why Unit Test? • Greater confidence than Buddy check only • Fewer regressions • Tests as documentation • don’t get out of step with the code • “Legacy Code is code without Unit Tests” – Michael Feathers • Can drive out clean designs
When to write Unit Tests? • ALWAYS • Test Before (TDD) tends to lead to cleaner interfaces • Test After tends to miss some test cases & takes longer • For TDD to work the component under test & the tests must build fast (< 1 minute) • You CAN make this possible • Componentise • Partition
Unit Test guidelines • Only test a single behaviour • Use descriptive names (as long as necessary) • Group related tests • Do not make tests brittle • Treat tests just like production code • Refactor to remove redundancy & improve architecture • Adhere to all coding standards • Tests are documentation • They must ‘read well’
How to write the first Unit Test • Major refactoring needed to put “seams” in place • Patterns used extensively for initial refactoring:“Working Effectively With Legacy Code” • Link errors in unit test build point to unwanted dependencies • Replace dependencies with ‘injected’ mock/fake objects …… until you really are UNIT testing.
A test is not a unit test if: • It talks to the database • It communicates across the network • It touches the file system • It can’t run at the same time as other unit tests • You have to do special things to your environment (such as editing config files) to run it (Michael Feathers’ blog, 2005)
Agenda • Background • Unit Testing • Frameworks • Test & Mock: First Examples • Refactoring: Wrap Dependency • Refactoring: Extract Component • Refactoring: Non-Intrusive C Seam • Conclusions • Questions
Which framework to use? • We chose Googletest & Googlemock • Available from Googlecode • Very liberal open source license • Cross platform • Can use independently, but work together “out of the box” • Implemented using macros & templates • Easy to learn • Well documented
Googletest • No need to register tests • Builds as command line executable • Familiar to users of xUnit: • Suites • Fixtures • SetUp, TearDown • Filters to enable running subsets • Handles exceptions
Googlemock • Feature-rich • Dependency on C++ TC1, but can use Boost • Extensible matching operators • Declarative style (using operator chaining) • Sequencing can be enforced • Use of templates slows build time • Can only mock virtual methods • Still need to declare mock interface • Inconvenient to mock operators, destructors & vararg
Agenda • Background • Unit Testing • Frameworks • Test & Mock: First Examples • Refactoring: Wrap Dependency • Refactoring: Extract Component • Refactoring: Non-Intrusive C Seam • Conclusions • Questions
The first test TEST(HttpResponse, default_response_code_should_be_unset) { HttpResponse response; ASSERT_EQ(HttpResponse::Unset, response.getCode()); }
The first mock (1) class RestfulServer { virtual bool doesDirectoryExist(const std::string& name) = 0; virtual bool doesResourceExist(const std::string& name) = 0; }; class MockRestfulServer : public RestfulServer { MOCK_METHOD1(doesDirectoryExist, bool(const std::string& name)); MOCK_METHOD1(doesResourceExist, bool(const std::string& name)); };
The first mock (2) TEST(JazzProxy_fileExists, should_return_true_if_directory_exists) { MockRestfulServer mockServer; Proxy proxy(mockServer); EXPECT_CALL(mockServer,doesDirectoryExist(_)) .WillOnce(Return(true)); EXPECT_CALL(mockServer, doesResourceExist(_)) .Times(0); bool exists = false; ASSERT_NO_THROW(proxy.fileExists(“myFolder", exists) ); ASSERT_TRUE(exists); }
Another Mock (1) HttpTimer::~HttpTimer() { if (theLogger.getLevel() >= LOG_LEVEL_WARNING) theLogger.writeLn(“Timer: %d ms", stopClock()); } class Logger { public: virtual ~Logger(); // Operations for logging textual entries to a log file. virtual unsigned getLevel() const = 0; virtual void write(const char* fmt, ...) = 0; virtual void writeLn(const char* fmt, ...) = 0; };
Another Mock (2) class MockLogger : public Logger { public: MOCK_CONST_METHOD0(getLevel, unsigned int()); void write(const char* fmt, ...) {}; void writeLn(const char* fmt, ...) { va_list ap; va_start(ap, fmt); DWORD clock = va_arg(ap, DWORD); va_end(ap); mockWriteLn(fmt, clock); } MOCK_METHOD2(mockWriteLn,void(const char*, DWORD)); };
Another Mock (3) TEST(HttpTimer, writes_to_logger_if_log_level_is_at_warning) { MockLogger testLogger; EXPECT_CALL(testLogger, getLevel()) .WillOnce(Return(LOG_LEVEL_WARNING)); EXPECT_CALL(testLogger, mockWriteLn( _, _)) .Times(1); HttpTimer timer(testLogger); }
Agenda • Background • Unit Testing • Frameworks • Test & Mock: First Examples • Refactoring: Wrap Dependency • Refactoring: Extract Component • Refactoring: Non-Intrusive C Seam • Conclusions • Questions
Wrap Dependency CONTEXT • We want to test some legacy code • The legacy code has an ugly dependency • Requires inclusion of code we don’t want to test SOLUTION • Create an interface that describes behaviour of dependency • Re-write call to inject dependency • In test code inject a test double
Test Doubles • Dummy: never used – only passed around to fill parameter list • Stub: provides canned responses • Fake: has simplified implementation • Mock: object pre-programmed with expectations – the specification of calls they are expected to receive • “Test Double”: generic term for any of the above
Code Under Test tree* openBaseline(tree *module, VersionId version) { tree *baseline = NULL; … BaselineId baselineId = DoorsServer::getInstance().findBaseline( module, version); … return baseline; }
Test The Defect TEST(OpenBaseline, opening_a_baseline_with_default_version_should_throw) { tree myTree; VersionId version; ASSERT_THROWS_ANY(openBaseline(&myTree, version)); } • Won’t link without inclusion of DoorsServer
Describe Behaviour class Server { virtual BaselineId findBaseline(tree*, VersionId) = 0; } class DoorsServer : public Server { … BaselineId findBaseline(tree*, VersionId); … }
Refactor Code Under Test tree* openBaseline(Server& server,tree *module, VersionId version) { tree *baseline = NULL; … BaselineId baselineId = server.findBaseline( module, version); … return baseline; }
Modify the Test class TestServer : public Server{ BaselineId findBaseline(tree*, VersionId) { return BaselineId(); } }; TEST(OpenBaseline, opening_a_baseline_with_default_version_should_throw) { TestServer server; tree myTree; VersionId version; ASSERT_THROWS_ANY( openBaseline(server, &myTree, version)); }
After the test passes • Modify all call sites openBaseline(t, version); becomes openBaseline(DoorsServer::getInstance(), t, version); • Add more methods to the interface as necessary • Consider cohesion • Don’t mindlessly create a monster interface • A similar result can be achieved without introducing an interface at all.
Agenda • Background • Unit Testing • Frameworks • Test & Mock: First Examples • Refactoring: Wrap Dependency • Refactoring: Extract Component • Refactoring: Non-Intrusive C Seam • Conclusions • Questions
Extract Component CONTEXT • All our code has dependency on ‘utility’ functionality • Some ‘utility’ functionality has dependencies on core application • Leads to linking test with entire codebase SOLUTION • Build ‘utility’ functionality as independent component • used by application and tests
Before Refactoring During Unit Test While App Executes Application Application Interesting Code Interesting Code main Tests
Simple Extraction Not Enough Application Utility Functionality Interesting Code • Utility code still dependent on app • No build time improvement main Tests
Break Dependency PROCEDURE • Create new interface(s) for dependencies of ‘utility’ class UserNotifier { virtual void notify(char*) =0; }; • Implement interface in application code class DoorsUserNotifier : public UserNotifier { virtual void notify(char*) { … } }; • Inject implementation of interface into ‘utility’ at initialisation DoorsUserNotifier userNotifier; utility.setUserNotifier(userNotifier);
Modify Utility Code • Interface registration void Utility::setUserNotifier(UserNotifier notifier) { userNotifier = notifier; } • Modify call sites in ‘utility’ to use injected interface • If no implementation present (i.e. during unit testing), then use of interface does nothing void Utility::notifyUser(char* message) { if (!userNotifier.isNull()) userNotifier->notify(message); }
Full extraction • Utility code is used in many places • All test projects will depend on it • Package as shared library • Reduces build times • Helps keep contracts explicit
After Refactoring While App Executes During Unit Test <<interface>> Application Dependencies <<interface>> Mock Dependencies Application Application Utility Functionality Utility Functionality Interesting Code Interesting Code 2. Run Application 1. Inject Dependencies main Tests
Agenda • Background • Unit Testing • Frameworks • Test & Mock: First Examples • Refactoring: Wrap Dependency • Refactoring: Extract Component • Refactoring: Non-Intrusive C Seam • Conclusions • Questions
Original code // startup.c void startup() { db_initialize(); … } // database.h extern void db_initialize(); // database.c void db_initialize() { … } db_initialize startup
How to unit test? • We want to test the startup method, but we don’t want to use the database • How can we test startup without calling db_initialize? • Use preprocessor • Use runtime switch • Supply ‘mock’ database object • The Mocking solution is the most versatile • … but also the most complex
Non-Intrusive C Seam CONTEXT • We want to replace some existing functionality • The functionality is implemented by procedural C code with no well defined interface • We don’t want to modify the ‘client’ code that uses this functionality SOLUTION • Create/extract an interface • Use C++ namespaces to silently redirect client calls through a factory/shim
Create new interface // Database.h class Database { virtual void initialize() = 0; …. }; Database db_initialize startup
Move legacy code into namespace // database.h namespace Legacy { extern void db_initialize(); } // database.c namespace Legacy { void db_initialize() { … } } Database startup db_initialize Legacy namespace Global namespace
Implement the new interface // LegacyDatabase.h class LegacyDatabase : public Database { void initialize(); }; // LegacyDatabase.cpp void LegacyDatabase::initialize() { Legacy::db_initialize(); } Database LegacyDatabase startup db_initialize Legacy namespace Global namespace
Create a shim // shim.h extern void db_initialize(); // shim.cpp void db_initialize() { Factory::getDatabase() .initialize(); } Database shim LegacyDatabase startup db_initialize Legacy namespace Global namespace
Redirect client to shim // startup.c #include “shim.h” void startup() { db_initialize(); … } Database shim LegacyDatabase startup db_initialize Legacy namespace Global namespace
Schematic of transformation Before After Database db_initialize shim LegacyDatabase startup startup db_initialize Global namespace Legacy namespace Global namespace