• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
Stopping the Rot - Putting Legacy C++ Under Test
 

Stopping the Rot - Putting Legacy C++ Under Test

on

  • 3,672 views

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.

Statistics

Views

Total Views
3,672
Views on SlideShare
3,034
Embed Views
638

Actions

Likes
3
Downloads
48
Comments
0

11 Embeds 638

http://claysnow.co.uk 565
http://claysnow.blogspot.com 49
http://posterous.com 6
http://claysnow.blogspot.co.uk 6
http://www.linkedin.com 5
http://honyaku.yahoofs.jp 2
http://claysnow.blogspot.ie 1
http://claysnow.blogspot.it 1
http://www.netvibes.com 1
http://claysnow.blogspot.com.au 1
https://www.linkedin.com 1
More...

Accessibility

Categories

Upload Details

Uploaded via as Microsoft PowerPoint

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment
  • Spaghetti code; Big Ball of Mud
  • “ Islands of test”
  • Spaghetti code; Big Ball of Mud

Stopping the Rot - Putting Legacy C++ Under Test Stopping the Rot - Putting Legacy C++ Under Test Presentation Transcript

  • 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 M ockRestfulServer : 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 _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);
    • }
  • 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;
    • };
  • 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));
    • } ;
  • 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);
    • }
  • 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
  • Tests Application Before Refactoring During Unit Test Interesting Code Application While App Executes main Interesting Code
  • Simple Extraction Not Enough Interesting Code Utility Functionality Application main
    • Utility code still dependent on app
    • No build time improvement
    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
  • 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
  • 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;
    • … .
    • };
    db_initialize startup Database
  • 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
  • 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
  • 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
  • Redirect client to shim
    • // startup.c
    • #include “shim.h”
    • void startup()
    • {
    • db_initialize();
    • }
    startup db_initialize shim Database LegacyDatabase Global namespace Legacy namespace
  • Schematic of transformation db_initialize startup Before After Global namespace startup db_initialize shim Database LegacyDatabase Global namespace Legacy namespace
  • 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
  • Agenda
    • Background
    • Unit Testing
    • Frameworks
    • Test & Mock: First Examples
    • Refactoring: Wrap Dependency
    • Refactoring: Extract Component
    • Refactoring: Non-Intrusive C Seam
    • Conclusions
    • Questions
  • 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
  • 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
  • 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
  • Agenda
    • Background
    • Unit Testing
    • Frameworks
    • Test & Mock: First Examples
    • Refactoring: Wrap Dependency
    • Refactoring: Extract Component
    • Refactoring: Non-Intrusive C Seam
    • Conclusions
    • Questions