Stopping the Rot - Putting Legacy C++ Under Test

  • 3,402 views
Uploaded on

Presentation given at the ACCU 2011 Conference in Oxford, UK. …

Presentation given at the ACCU 2011 Conference in Oxford, UK.

Case study of applying unit test to the DOORS codebase. Includes a quick overview of unit test & the Google Test and Mock libraries. Also 3 specific refactoring examples shown.

More in: Technology
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
3,402
On Slideshare
0
From Embeds
0
Number of Embeds
5

Actions

Shares
Downloads
53
Comments
0
Likes
4

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide
  • Spaghetti code; Big Ball of Mud
  • “ Islands of test”
  • Spaghetti code; Big Ball of Mud

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 M ockRestfulServer : 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 _r eturn _t rue _i f _directory_e xists)
    • {
    • MockRestfulServer mockServer;
    • Proxy p roxy(mockServer);
    • EXPECT_CALL(mockServer, doesDirectoryExist( _ ))
    • .WillOnce(Return(true));
    • EXPECT_CALL(mockServer, doesResourceExist(_))
    • .Times(0);
    • bool exists = false;
    • ASSERT_NO_THROW(proxy. fileExists(“ myFolder &quot;, exists) ) ;
    • ASSERT_TRUE(exists);
    • }
  • 21. Another Mock (1)
    • HttpTimer::~HttpTimer()
    • {
    • if (theLogger.getLevel() >= LOG_LEVEL_WARNING)
    • theLogger.writeLn(“ Timer: %d ms&quot;, 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_METHOD 2 (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())
    • .Will Once (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. Tests Application Before Refactoring During Unit Test Interesting Code Application While App Executes main Interesting Code
  • 36. Simple Extraction Not Enough Interesting Code Utility Functionality Application main
    • Utility code still dependent on app
    • No build time improvement
    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. Tests After Refactoring During Unit Test Utility Functionality <<interface>> Mock Dependencies Application Interesting Code Utility Functionality <<interface>> Application Dependencies Application While App Executes main Interesting Code 1. Inject Dependencies 2. Run Application
  • 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;
    • … .
    • };
    db_initialize startup Database
  • 46. Move legacy code into namespace
    • // database.h
    • namespace Legacy
    • {
    • extern void db_initialize();
    • }
    • // database.c
    • namespace Legacy
    • {
    • void db_initialize()
    • {
    • }
    • }
    startup db_initialize Global namespace Legacy namespace Database
  • 47. Implement the new interface
    • // LegacyDatabase.h
    • class LegacyDatabase : public Database
    • {
    • void initialize();
    • };
    • // LegacyDatabase.cpp
    • void LegacyDatabase::initialize()
    • {
    • Legacy::db_initialize();
    • }
    startup Global namespace db_initialize Database LegacyDatabase Legacy namespace
  • 48. Create a shim
    • // shim.h
    • extern void db_initialize();
    • // shim.cpp
    • void db_initialize()
    • {
    • Factory::getDatabase()
    • .initialize();
    • }
    startup db_initialize shim Database LegacyDatabase Global namespace Legacy namespace
  • 49. Redirect client to shim
    • // startup.c
    • #include “shim.h”
    • void startup()
    • {
    • db_initialize();
    • }
    startup db_initialize shim Database LegacyDatabase Global namespace Legacy namespace
  • 50. Schematic of transformation db_initialize startup Before After Global namespace startup db_initialize shim Database LegacyDatabase Global namespace Legacy namespace
  • 51. What have we achieved?
    • Extracted an interface with minimal changes to client code
    • Original invocation now calls shim code
    • Shim uses factory to select implementation
    • Factory can return a fake or mock object
    • Legacy implementation behaves exactly as before
    • Code can be unit tested independently
    • Alternative implementations of interface can be provided
  • 52. Agenda
    • Background
    • Unit Testing
    • Frameworks
    • Test & Mock: First Examples
    • Refactoring: Wrap Dependency
    • Refactoring: Extract Component
    • Refactoring: Non-Intrusive C Seam
    • Conclusions
    • Questions
  • 53. Are we there yet?
    • Move to iterative development
    • All new/modified code to have unit tests
    • All unit tests to be run every build
    • Develop “Whole Team” approach
      • Automated acceptance tests written by test & dev
      • Test to pick up nightly builds
    • Align with Rational toolset
  • 54. Conclusions
    • New skills/techniques to learn
      • Unit testing is hard
      • Writing testable code is hard
      • Books are not enough… practice needed
    • Up-front refactoring cost
      • Lots of hard work making legacy code testable
      • One step at a time
    • Build times are important to developers
      • But other metrics are equally interesting
  • 55. Musical trivia
    • You can lead a horse to water,
    • But you can’t make it drink
    • Think about it,
    • All you’ve got to do is think
    • about it
    • There’s no cure.
    • - The Beast, The Only Ones
  • 56. Agenda
    • Background
    • Unit Testing
    • Frameworks
    • Test & Mock: First Examples
    • Refactoring: Wrap Dependency
    • Refactoring: Extract Component
    • Refactoring: Non-Intrusive C Seam
    • Conclusions
    • Questions