1 / 56

Stopping the rot

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

Télécharger la présentation

Stopping the rot

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. Stopping the rot Putting legacy C++ under test Seb Rose ACCU 2011

  2. Agenda • Background • Unit Testing • Frameworks • Test & Mock: First Examples • Refactoring: Wrap Dependency • Refactoring: Extract Component • Refactoring: Non-Intrusive C Seam • Conclusions • Questions

  3. Agenda • Background • Unit Testing • Frameworks • Test & Mock: First Examples • Refactoring: Wrap Dependency • Refactoring: Extract Component • Refactoring: Non-Intrusive C Seam • Conclusions • Questions

  4. 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

  5. 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

  6. 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

  7. Agenda • Background • Unit Testing • Frameworks • Test & Mock: First Examples • Refactoring: Wrap Dependency • Refactoring: Extract Component • Refactoring: Non-Intrusive C Seam • Conclusions • Questions

  8. 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

  9. 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

  10. 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’

  11. 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.

  12. 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)

  13. Agenda • Background • Unit Testing • Frameworks • Test & Mock: First Examples • Refactoring: Wrap Dependency • Refactoring: Extract Component • Refactoring: Non-Intrusive C Seam • Conclusions • Questions

  14. 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

  15. 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

  16. 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

  17. Agenda • Background • Unit Testing • Frameworks • Test & Mock: First Examples • Refactoring: Wrap Dependency • Refactoring: Extract Component • Refactoring: Non-Intrusive C Seam • Conclusions • Questions

  18. The first test TEST(HttpResponse, default_response_code_should_be_unset) { HttpResponse response; ASSERT_EQ(HttpResponse::Unset, response.getCode()); }

  19. 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)); };

  20. 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); }

  21. 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; };

  22. 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)); };

  23. 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); }

  24. Agenda • Background • Unit Testing • Frameworks • Test & Mock: First Examples • Refactoring: Wrap Dependency • Refactoring: Extract Component • Refactoring: Non-Intrusive C Seam • Conclusions • Questions

  25. 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

  26. 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

  27. Code Under Test tree* openBaseline(tree *module, VersionId version) { tree *baseline = NULL; … BaselineId baselineId = DoorsServer::getInstance().findBaseline( module, version); … return baseline; }

  28. 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

  29. Describe Behaviour class Server { virtual BaselineId findBaseline(tree*, VersionId) = 0; } class DoorsServer : public Server { … BaselineId findBaseline(tree*, VersionId); … }

  30. Refactor Code Under Test tree* openBaseline(Server& server,tree *module, VersionId version) { tree *baseline = NULL; … BaselineId baselineId = server.findBaseline( module, version); … return baseline; }

  31. 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)); }

  32. 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.

  33. Agenda • Background • Unit Testing • Frameworks • Test & Mock: First Examples • Refactoring: Wrap Dependency • Refactoring: Extract Component • Refactoring: Non-Intrusive C Seam • Conclusions • Questions

  34. 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

  35. Before Refactoring During Unit Test While App Executes Application Application Interesting Code Interesting Code main Tests

  36. Simple Extraction Not Enough Application Utility Functionality Interesting Code • Utility code still dependent on app • No build time improvement main Tests

  37. 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);

  38. 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); }

  39. 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

  40. 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

  41. Agenda • Background • Unit Testing • Frameworks • Test & Mock: First Examples • Refactoring: Wrap Dependency • Refactoring: Extract Component • Refactoring: Non-Intrusive C Seam • Conclusions • Questions

  42. Original code // startup.c void startup() { db_initialize(); … } // database.h extern void db_initialize(); // database.c void db_initialize() { … } db_initialize startup

  43. 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

  44. 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

  45. Create new interface // Database.h class Database { virtual void initialize() = 0; …. }; Database db_initialize startup

  46. 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

  47. 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

  48. 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

  49. Redirect client to shim // startup.c #include “shim.h” void startup() { db_initialize(); … } Database shim LegacyDatabase startup db_initialize Legacy namespace Global namespace

  50. Schematic of transformation Before After Database db_initialize shim LegacyDatabase startup startup db_initialize Global namespace Legacy namespace Global namespace

More Related