Upload
tryphena-malinda
View
31
Download
2
Embed Size (px)
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
Citation preview
®
IBM Software Group
© 2009 IBM CorporationInnovation for a smarter planet
Stopping the rotPutting legacy C++ under test
Seb RoseACCU 2011
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
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
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
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
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
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
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
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
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’
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.
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)
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
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
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
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
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
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());
}
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));
};
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);
}
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;
};
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));
};
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);
}
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
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
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
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;
}
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
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);
…
}
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;
}
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));
}
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.
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
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
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
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
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);
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);
}
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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