stopping the rot

56
® IBM Software Group © 2009 IBM Corporation Innovation for a smarter planet Stopping the rot Putting legacy C++ under test Seb Rose ACCU 2011

Upload: tryphena-malinda

Post on 30-Dec-2015

31 views

Category:

Documents


2 download

DESCRIPTION

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 - PowerPoint PPT Presentation

TRANSCRIPT

Page 1: Stopping the rot

®

IBM Software Group

© 2009 IBM CorporationInnovation for a smarter planet

Stopping the rotPutting legacy C++ under test

Seb RoseACCU 2011

Page 2: Stopping the rot

IBM Software Group | Rational software

2Innovation for a smarter planet

Agenda

Background

Unit Testing

Frameworks

Test & Mock: First Examples

Refactoring: Wrap Dependency

Refactoring: Extract Component

Refactoring: Non-Intrusive C Seam

Conclusions

Questions

Page 3: Stopping the rot

IBM Software Group | Rational software

3Innovation for a smarter planet

Agenda

Background

Unit Testing

Frameworks

Test & Mock: First Examples

Refactoring: Wrap Dependency

Refactoring: Extract Component

Refactoring: Non-Intrusive C Seam

Conclusions

Questions

Page 4: Stopping the rot

IBM Software Group | Rational software

4Innovation for a smarter planet

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

Page 5: Stopping the rot

IBM Software Group | Rational software

5Innovation for a smarter planet

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

Page 6: Stopping the rot

IBM Software Group | Rational software

6Innovation for a smarter planet

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 buildNightly builds

CI server

Develop “Whole Team” approachAutomated acceptance tests written by test & dev

Test to pick up nightly builds

Align with Rational toolset

Page 7: Stopping the rot

IBM Software Group | Rational software

7Innovation for a smarter planet

Agenda

Background

Unit Testing

Frameworks

Test & Mock: First Examples

Refactoring: Wrap Dependency

Refactoring: Extract Component

Refactoring: Non-Intrusive C Seam

Conclusions

Questions

Page 8: Stopping the rot

IBM Software Group | Rational software

8Innovation for a smarter planet

Why Unit Test?

Greater confidence than Buddy check only

Fewer regressions

Tests as documentationdon’t get out of step with the code

“Legacy Code is code without Unit Tests” – Michael Feathers

Can drive out clean designs

Page 9: Stopping the rot

IBM Software Group | Rational software

9Innovation for a smarter planet

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 possibleComponentisePartition

Page 10: Stopping the rot

IBM Software Group | Rational software

10Innovation for a smarter planet

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’

Page 11: Stopping the rot

IBM Software Group | Rational software

11Innovation for a smarter planet

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.

Page 12: Stopping the rot

IBM Software Group | Rational software

12Innovation for a smarter planet

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)

Page 13: Stopping the rot

IBM Software Group | Rational software

13Innovation for a smarter planet

Agenda

Background

Unit Testing

Frameworks

Test & Mock: First Examples

Refactoring: Wrap Dependency

Refactoring: Extract Component

Refactoring: Non-Intrusive C Seam

Conclusions

Questions

Page 14: Stopping the rot

IBM Software Group | Rational software

14Innovation for a smarter planet

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

Page 15: Stopping the rot

IBM Software Group | Rational software

15Innovation for a smarter planet

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

Page 16: Stopping the rot

IBM Software Group | Rational software

16Innovation for a smarter planet

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

Page 17: Stopping the rot

IBM Software Group | Rational software

17Innovation for a smarter planet

Agenda

Background

Unit Testing

Frameworks

Test & Mock: First Examples

Refactoring: Wrap Dependency

Refactoring: Extract Component

Refactoring: Non-Intrusive C Seam

Conclusions

Questions

Page 18: Stopping the rot

IBM Software Group | Rational software

18Innovation for a smarter planet

The first test

TEST(HttpResponse, default_response_code_should_be_unset)

{

HttpResponse response;

ASSERT_EQ(HttpResponse::Unset, response.getCode());

}

Page 19: Stopping the rot

IBM Software Group | Rational software

19Innovation for a smarter planet

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

};

Page 20: Stopping the rot

IBM Software Group | Rational software

20Innovation for a smarter planet

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

}

Page 21: Stopping the rot

IBM Software Group | Rational software

21Innovation for a smarter planet

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;

};

Page 22: Stopping the rot

IBM Software Group | Rational software

22Innovation for a smarter planet

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

};

Page 23: Stopping the rot

IBM Software Group | Rational software

23Innovation for a smarter planet

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

}

Page 24: Stopping the rot

IBM Software Group | Rational software

24Innovation for a smarter planet

Agenda Background

Unit Testing

Frameworks

Test & Mock: First Examples

Refactoring: Wrap Dependency

Refactoring: Extract Component

Refactoring: Non-Intrusive C Seam

Conclusions

Questions

Page 25: Stopping the rot

IBM Software Group | Rational software

25Innovation for a smarter planet

Wrap Dependency

CONTEXT

We want to test some legacy code

The legacy code has an ugly dependencyRequires 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

Page 26: Stopping the rot

IBM Software Group | Rational software

26Innovation for a smarter planet

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

Page 27: Stopping the rot

IBM Software Group | Rational software

27Innovation for a smarter planet

Code Under Test tree* openBaseline(tree *module, VersionId version)

{

tree *baseline = NULL;

BaselineId baselineId = DoorsServer::getInstance().findBaseline( module, version);

return baseline;

}

Page 28: Stopping the rot

IBM Software Group | Rational software

28Innovation for a smarter planet

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

Page 29: Stopping the rot

IBM Software Group | Rational software

29Innovation for a smarter planet

Describe Behaviourclass Server

{

virtual BaselineId findBaseline(tree*, VersionId) = 0;

}

class DoorsServer : public Server

{

BaselineId findBaseline(tree*, VersionId);

}

Page 30: Stopping the rot

IBM Software Group | Rational software

30Innovation for a smarter planet

Refactor Code Under Test tree* openBaseline(

Server& server,tree *module, VersionId version)

{

tree *baseline = NULL;

BaselineId baselineId = server.findBaseline( module, version);

return baseline;

}

Page 31: Stopping the rot

IBM Software Group | Rational software

31Innovation for a smarter planet

Modify the Testclass 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));

}

Page 32: Stopping the rot

IBM Software Group | Rational software

32Innovation for a smarter planet

After the test passes Modify all call sites

openBaseline(t, version);

becomes

openBaseline(DoorsServer::getInstance(), t, version);

Add more methods to the interface as necessaryConsider cohesion

Don’t mindlessly create a monster interface

A similar result can be achieved without introducing an interface at all.

Page 33: Stopping the rot

IBM Software Group | Rational software

33Innovation for a smarter planet

Agenda

Background

Unit Testing

Frameworks

Test & Mock: First Examples

Refactoring: Wrap Dependency

Refactoring: Extract Component

Refactoring: Non-Intrusive C Seam

Conclusions

Questions

Page 34: Stopping the rot

IBM Software Group | Rational software

34Innovation for a smarter planet

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

Page 35: Stopping the rot

IBM Software Group | Rational software

35Innovation for a smarter planet

Tests

Application

Before RefactoringDuring Unit Test

Interesting Code

Application

While App Executes

main

Interesting Code

Page 36: Stopping the rot

IBM Software Group | Rational software

36Innovation for a smarter planet

Simple Extraction Not Enough

Interesting Code

UtilityFunctionality

Application

main Utility code still dependent on app

No build time improvementTests

Page 37: Stopping the rot

IBM Software Group | Rational software

37Innovation for a smarter planet

Break DependencyPROCEDURE

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

Page 38: Stopping the rot

IBM Software Group | Rational software

38Innovation for a smarter planet

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

}

Page 39: Stopping the rot

IBM Software Group | Rational software

39Innovation for a smarter planet

Full extraction

Utility code is used in many places

All test projects will depend on it

Package as shared libraryReduces build times

Helps keep contracts explicit

Page 40: Stopping the rot

IBM Software Group | Rational software

40Innovation for a smarter planet

Tests

After RefactoringDuring Unit Test

UtilityFunctionality

<<interface>>Mock

Dependencies

Application

Interesting Code

UtilityFunctionality

<<interface>>Application

Dependencies

Application

While App Executes

main

Interesting Code

1. Inject Dependencies

2. Run Application

Page 41: Stopping the rot

IBM Software Group | Rational software

41Innovation for a smarter planet

Agenda

Background

Unit Testing

Frameworks

Test & Mock: First Examples

Refactoring: Wrap Dependency

Refactoring: Extract Component

Refactoring: Non-Intrusive C Seam

Conclusions

Questions

Page 42: Stopping the rot

IBM Software Group | Rational software

42Innovation for a smarter planet

Original code// startup.c

void startup()

{

db_initialize();

}

// database.h

extern void db_initialize();

// database.c

void db_initialize()

{

}

db_initialize

startup

Page 43: Stopping the rot

IBM Software Group | Rational software

43Innovation for a smarter planet

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

Page 44: Stopping the rot

IBM Software Group | Rational software

44Innovation for a smarter planet

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

Page 45: Stopping the rot

IBM Software Group | Rational software

45Innovation for a smarter planet

Create new interface

// Database.h

class Database

{

virtual void initialize() = 0;

….

};

db_initialize

startup

Database

Page 46: Stopping the rot

IBM Software Group | Rational software

46Innovation for a smarter planet

Move legacy code into namespace// database.h

namespace Legacy

{

extern void db_initialize();

}

// database.c

namespace Legacy

{

void db_initialize()

{

}

}

startup db_initialize

Global namespaceLegacy namespace

Database

Page 47: Stopping the rot

IBM Software Group | Rational software

47Innovation for a smarter planet

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

Page 48: Stopping the rot

IBM Software Group | Rational software

48Innovation for a smarter planet

Create a shim

// shim.h

extern void db_initialize();

// shim.cpp

void db_initialize()

{

Factory::getDatabase()

.initialize();

}

startup db_initialize

shim

Database

LegacyDatabase

Global namespaceLegacy namespace

Page 49: Stopping the rot

IBM Software Group | Rational software

49Innovation for a smarter planet

Redirect client to shim

// startup.c

#include “shim.h”

void startup()

{

db_initialize();

}

startup db_initialize

shim

Database

LegacyDatabase

Global namespaceLegacy namespace

Page 50: Stopping the rot

IBM Software Group | Rational software

50Innovation for a smarter planet

Schematic of transformation

db_initialize

startup

Before After

Global namespace

startup db_initialize

shim

Database

LegacyDatabase

Global namespaceLegacy namespace

Page 51: Stopping the rot

IBM Software Group | Rational software

51Innovation for a smarter planet

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

Page 52: Stopping the rot

IBM Software Group | Rational software

52Innovation for a smarter planet

Agenda

Background

Unit Testing

Frameworks

Test & Mock: First Examples

Refactoring: Wrap Dependency

Refactoring: Extract Component

Refactoring: Non-Intrusive C Seam

Conclusions

Questions

Page 53: Stopping the rot

IBM Software Group | Rational software

53Innovation for a smarter planet

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” approachAutomated acceptance tests written by test & dev

Test to pick up nightly builds

Align with Rational toolset

Page 54: Stopping the rot

IBM Software Group | Rational software

54Innovation for a smarter planet

Conclusions

New skills/techniques to learnUnit testing is hardWriting testable code is hardBooks are not enough… practice needed

Up-front refactoring costLots of hard work making legacy code testableOne step at a time

Build times are important to developersBut other metrics are equally interesting

Page 55: Stopping the rot

IBM Software Group | Rational software

55Innovation for a smarter planet

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

Page 56: Stopping the rot

IBM Software Group | Rational software

56Innovation for a smarter planet

Agenda

Background

Unit Testing

Frameworks

Test & Mock: First Examples

Refactoring: Wrap Dependency

Refactoring: Extract Component

Refactoring: Non-Intrusive C Seam

Conclusions

Questions