Presented at ACCU York, on 25th April 2019.
You've inherited some legacy code: it's valuable, but it doesn't have tests, and it wasn't designed to be testable, so you need to start refactoring! But you can't refactor safely until the code has tests, and you can't add tests without refactoring. How can you ever break out of this loop?
I present a new C++ library for applying Llewellyn Falco's "Approval Tests" approach to testing cross-platform C++ code - for both legacy and green-field systems, and a range of testing frameworks.
I describe its use in some real-world situations, including how to quickly lock down the behaviour of legacy code. I show how to quickly achieve good test coverage, even for very large sets of inputs. Finally, I also describe some general techniques I learned along the way.
13. 13
@ClareMacraeUK
What does Legacy Code really mean?
• Michael Feathers
–“code without unit tests”
• J.B. Rainsberger
–“profitable code that we feel afraid to change.”
• Kate Gregory
–“any code that you want to change, but are afraid to”
14. 14
@ClareMacraeUK
Typical Scenario
• I've inherited some
legacy code
• It's valuable
• I need to add feature
• Or fix bug
• How can I ever break
out of this loop?
Need to
change
the code
No tests
Not
designed
for testing
Needs
refactoring
to add
tests
Can’t
refactor
without
tests
19. 19
@ClareMacraeUK
Popular C++ Test Frameworks
Google Test
• Google's C++ test framework
• https://github.com/google/googletest
Catch
• Phil Nash’s test framework
• https://github.com/catchorg/Catch2
36. 36
@ClareMacraeUK
Thoughts on Golden Master Tests
• Good to start testing legacy systems
• Poor Person’s Integration Tests
• Depends on ease of
– Capturing output
– Getting stable output
– Reviewing any differences
– Avoiding overwriting Master by mistake!
• Doesn’t matter that it’s not a Unit Test
39. 39
@ClareMacraeUK
One approach: “ApprovalTests”
• Llewellyn Falco’s convenient, powerful, flexible Golden Master implementation
• Supports many languages
And now C++!
GO
Java
Lua
.NET
.Net.ASP
NodeJS
Objective-C
PHP
Perl
Python
Swift
TCL
40. 40
@ClareMacraeUK
ApprovalTests.cpp
• New C++ library for applying Llewellyn Falco’s “Approval Tests” approach
• For testing cross-platform C++ code (Windows, Linux, Mac)
• For legacy and green-field systems
• It’s on github
45. 45
@ClareMacraeUK
How to use it: Catch2 Boilerplate
• Your main.cpp
#define APPROVALS_CATCH
#include "ApprovalTests.hpp"
46. 46
@ClareMacraeUK
Pure Catch2 Test
• A test file
#include "Catch.hpp"
// Catch-only test
TEST_CASE( "Sums are calculated" )
{
REQUIRE( 1 + 1 == 2 );
REQUIRE( 1 + 2 == 3 );
}
47. 47
@ClareMacraeUK
Approvals Catch2 Test
• A test file (Test02.cpp)
#include "ApprovalTests.hpp"
#include "Catch.hpp"
// Approvals test - test static value, for demo purposes
TEST_CASE("TestFixedInput")
{
Approvals::verify("SomenMulti-linenoutput");
}
52. 52
@ClareMacraeUK
Second run
• I approved the output
• Then we commit the test, and the approved file(s) to version control
• The diffing tool only shows up if:
– there is not (yet) an Approved file or
– the Received differs from the Approved
53. 53
@ClareMacraeUK
What’s going on here?
• It’s a very convenient form of Golden Master testing
• Objects being tested are stringified – that’s a requirement
• Captures snapshot of current behaviour
• Useful for locking down behaviour of existing code
– Not intended to replace Unit Tests
55. 55
@ClareMacraeUK
How to use it: Catch2 Boilerplate
• Your main.cpp
#define APPROVALS_CATCH
#include "ApprovalTests.hpp“
// Put all approved and received files in "approval_tests/"
auto dir =
Approvals::useApprovalsSubdirectory("approval_tests");
56. 56
@ClareMacraeUK
How to use it: GoogleTest Boilerplate
• Your main.cpp
#define APPROVALS_GOOGLETEST
#include "ApprovalTests.hpp"
57. 57
@ClareMacraeUK
Pure Google Test
• A test file
#include <gtest/gtest.h>
// Google-only test
TEST( Test01, SumsAreCalculated )
{
EXPECT_EQ( 1 + 1, 2 );
EXPECT_EQ( 1 + 2, 3 );
}
58. 58
@ClareMacraeUK
Approvals Google Test
• A test file
#include "ApprovalTests.hpp"
#include <gtest/gtest.h>
// googletest - test static value, for demo purposes
TEST( Test02, TestFixedInput )
{
Approvals::verify("SomenMulti-linenoutput");
}
62. 64
@ClareMacraeUK
Applying this to legacy code
TEST_CASE("New test of legacy feature")
{
// Standard pattern:
// Wrap your legacy feature to test in a function call
// that returns some state that can be written to a text file
// for verification:
const LegacyThing result = doLegacyOperation();
Approvals::verify(result);
}
68. 70
@ClareMacraeUK
Feature: Quick to get good coverage
TEST_CASE("verifyAllCombinationsWithLambda")
{
std::vector<std::string> strings{"hello", "world"};
std::vector<int> numbers{1, 2, 3};
CombinationApprovals::verifyAllCombinations<
std::vector<std::string>, // The type of element in our first input container
std::vector<int>, // The type of element in our second input container
std::string>( // The return type from testing one combination of inputs
// Lambda that acts on one combination of inputs, and returns the result to be approved:
[](std::string s, int i) { return s + " " + std::to_string(i); },
strings, // The first input container
numbers); // The second input container
}
69. 71
@ClareMacraeUK
All values in single output file:
(hello, 1) => hello 1
(hello, 2) => hello 2
(hello, 3) => hello 3
(world, 1) => world 1
(world, 2) => world 2
(world, 3) => world 3
70. 72
@ClareMacraeUK
Customisability: Reporters
• Very simple but powerful abstraction
• Gives complete user control over how to inspect and act on a failure
• If you adopt Approvals, do review the supplied reporters for ideas
71. 73
@ClareMacraeUK
Feature: Change the Reporter in one test
TEST_CASE("Demo custom reporter")
{
std::vector<std::string> v{"hello", "world"};
Approvals::verifyAll(v, MyCustomReporter());
}
72. 74
@ClareMacraeUK
Feature: Change the default Reporter
// main.cpp - or in individual tests:
#include <memory>
auto disposer = Approvals::useAsDefaultReporter(
std::make_shared<Windows::BeyondCompare4Reporter>() );
auto disposer = Approvals::useAsDefaultReporter(
std::make_shared<GenericDiffReporter>(
"C:/Program Files/Araxis/Araxis Merge/Compare.exe") );
73. 75
@ClareMacraeUK
Feature: Don’t run GUIs on build servers
// main.cpp
auto disposer = Approvals::useAsFrontLoadedReporter(
BlockingReporter::onMachineNamed("MyCIMachineName") );
105. 107
@ClareMacraeUK
Create custom QImage comparison class
// Allow differences in up to 1/255 of RGB values at each pixel, as saved in 32-bit images
// sometimes have a few slightly different pixels, which are not visible to the human eye.
class QImageApprovalComparator : public ApprovalComparator
{
public:
bool contentsAreEquivalent(std::string receivedPath, std::string approvedPath) const override
{
const QImage receivedImage(QString::fromStdString(receivedPath));
const QImage approvedImage(QString::fromStdString(approvedPath));
return compareQImageIgnoringTinyDiffs(receivedImage, approvedImage);
}
};
106. 108
@ClareMacraeUK
Register the custom comparison class
// Somewhere in main…
FileApprover::registerComparator(
".png",
std::make_shared<QImageApprovalComparator>());
107. 109
@ClareMacraeUK
Does the difference matter?
• Legacy code is often brittle
• Testing makes changes visible
• Then decide if change matters
• Fast feedback cycle for efficient development
108. 110
@ClareMacraeUK
Looking back
• User perspective
• Learned about own code
• Enabled unit tests for graphics problems
• Approvals useful even for temporary tests
113. 115
@ClareMacraeUK
The Legacy Code Programmer’s Toolbox
• https://leanpub.com/legacycode
• The Legacy Code Programmer's Toolbox: Practical skills for software professionals working
with legacy code, by Jonathan Boccara
115. 117
@ClareMacraeUK
Adam Tornhill’s Books
• Mining actionable information from historical code bases (by Adam Tornhill)
– Your Code as a Crime Scene
– Software Design X-Rays: Fix Technical Debt with Behavioural Code Analysis
118. 120
@ClareMacraeUK
Our original goals
• Encourage under-represented people
– To speak
– To apply for jobs
– To stay in this industry
• Get conferences to have a code of conduct
– Hadn’t even thought about enforcement
• Get employers to value diversity somewhat
• Provide some resources to conferences and employers
120
119. 121
@ClareMacraeUK
What we did, April 2018 – April 2019
• Grew the discord server to over 1900 people
– 30+ public channels
– Plus more for “established” users
• Sent real people to conferences
– Admission provided by conference
– Fundraising for travel expenses
• Booth / table / stand at every major C++
conference
– Welcoming people and happy space
– Inviting people to Discord, providing answers, etc
• Got shirts, stickers, badges etc out into the
world
– Speakers wear them on stage
– They’ve been spotted at the ISO C++ meetings
• People changing how they speak and write
– Examples in demos
– “guys”
• And yes, got some conferences to add a CoC
– And real enforcement
121. 123
@ClareMacraeUK
Adopting legacy code
• You can do it!
• Evaluate current tests
• Quickly improve coverage with Golden Master
• ApprovalTests.cpp makes that really easy
• Even for non-text types
124. 126
@ClareMacraeUK
Thank You: Any Questions?
• Example Code: https://github.com/claremacrae/cpponsea2019
• Slides: https://www.slideshare.net/ClareMacrae
– (later this week)
• ApprovalTests.cpp
– https://github.com/approvals/ApprovalTests.cpp
– https://github.com/approvals/ApprovalTests.cpp.StarterProject
Notes de l'éditeur
I’ll tweet the location of slides (and perhaps sample code) later today
Google test:
Very powerful tool
Can be a pain to add to your builds
Catch:
As powerful as Google Test
Distributed as a Single Header
Much easier to incorporate in to builds
https://pragprog.com/book/lotdd/modern-c-programming-with-test-driven-development
Uses Google Mock and Google Test
Has a very useful chapter on dealing with Legacy Code, with worked examples.
Note of caution: knowing that a line of code is executed tells nothing about if it was tested!
Still useful to see totally un-tested code
Techniques
Run in debugger with breakpoints, to see what lines are reached
Run through code coverage tool to at least see what lines are executed
But what do you measure?
Another phrase for the Golden Master output is “Known Good” output.
These kinds of diagrams will probably be familiar to many from school chemistry lessons
They say there are Carbon, Oxygen and Hydrogen atoms in this “molecule”.
The lines represent the bonds between the atoms.
There are lots of conventions built in to this, such as the hydrogens here are collapsed down to the labels.
Chemistry software uses standard conventions for to colour-code by element. So Oxygen is often drawn as red.
We can see some kind of shape here, but what really is the shape of the sucrose molecule.
Now by changing the display style, we start to see more of the shape.
In fact, a crystal of sucrose is built from a potentially infinite set of repeating molecules.
Simon
Patricia
https://shop.spreadshirt.net/includecpp is the European online store