Refactoring for
testability in C++
Hard-to-test patterns in C++ and
how to refactor them
About me Dimitrios Platis
● Grew up in Rodos, Greece
● Software Engineer @ Edument,
Gothenburg
● Course responsible @ Gothenburg
University
● Interests:
○ Embedded systems
○ Software Architecture
○ API Design
○ Open source software & hardware
○ Robots, Portable gadgets, IoT
○ 3D printing
○ Autonomous Driving
● Website: https://platis.solutions
DIT112-V19
DIT112-V20
Agenda
1. GoogleTest & GoogleMock
2. Ηard-to-test code
3. Easy-to-test code
4. Patterns to refactor
GoogleTest &
GoogleMock
The de facto (?) unit test framework for C++
● xUnit test framework
● Runs on multiple platforms
● Open source
○ https://github.com/google/googletest
● Rich documentation & community
● Easy to integrate with CMake
● No fancy functionality
✔ Mocks, assertions, parameterized tests
✖ DI container, "spy"
Hard-to-test code
When is code hard to test?
● High complexity
● Tight coupling with other components
● Dependence on global/static states
● Sequential coupling
● ...more!
When
it's not fun
● Create fakes containing a lot of logic
● Change visibility of member functions and attributes
● Test things that have been tested before
● Sporadic fails
● Execution takes ages
Easy-to-test code
When is code easy to test?
● Low complexity
● SOLID
● Prefers composition over inheritance
● Abstracted dependencies
● Functional code without side-effects
● ...more!
Patterns to refactor
Grain of salt
● Treat these examples as generic guidelines
○ Won't necessarily apply to your project
● Maybe worse performance
○ 3 rules of optimization
● Possibly incompatible with your domain constraints
○ ISO 26262, AUTOSAR, MISRA
● Perhaps easier/cheaper with more advanced test frameworks
○ Fruit
Core philosophy
Getting own
dependencies
Managing own
lifecycle
Getting own
configuration
Ignorantly controlling self
Dependencies
injected
Lifecycle
managed from
outside
Configuration
injected
Inversion of Control
Adopted from picocontainer.com article
Files
Why is it difficult?
bool write(const std::string& filePath,
const std::string& content)
{
std::ofstream outfile(filePath.c_str(),
std::ios::trunc);
if (outfile.good())
{
outfile << content << std::endl;
}
return outfile.good();
}
std::optional<std::string>
read(const std::string& filePath)
{
std::ifstream fileToRead(filePath);
std::stringstream buffer;
buffer << fileToRead.rdbuf();
if (buffer.good())
{
return std::make_optional(buffer.str());
}
return std::nullopt;
}
How would you test this?
bool FileEncoder::encode(const std::string& filePath) const
{
const auto validFileContents = read(filePath);
if (!validFileContents)
{
return false;
}
auto encodedFileContents = validFileContents.value();
// Do something with file contents
const auto wroteFileSuccessfully
= write(filePath + ".encoded", encodedFileContents);
return wroteFileSuccessfully;
}
Refactor
Abstract
struct FileReader {
virtual ~FileReader() = default;
virtual std::optional<std::string>
read(const std::string& filePath) const = 0;
};
struct FileWriter {
virtual ~FileWriter() = default;
virtual bool
write(const std::string& filePath,
const std::string& content) const = 0;
};
Inject
struct FileEncoder {
FileEncoder(FileReader& fileReader,
FileWriter& fileWriter);
bool encode(const std::string& filePath) const;
private:
FileReader& mFileReader;
FileWriter& mFileWriter;
};
Hard-coded
dependencies
Why is it difficult?
struct DirectionlessOdometer
{
DirectionlessOdometer(int pulsePin,
int pulsesPerMeter);
double getDistance() const;
protected:
const int mPulsesPerMeter;
int mPulses{0};
MyInterruptServiceManager mInterruptManager;
};
struct DirectionalOdometer
: public DirectionlessOdometer
{
DirectionalOdometer(int directionPin,
int pulsePin,
int pulsesPerMeter);
private:
MyPinReader mPinReader;
const int mDirectionPin;
};
How would you test this?
DirectionlessOdometer::DirectionlessOdometer(
int pulsePin, int pulsesPerMeter)
: mPulsesPerMeter{pulsesPerMeter}
{
mInterruptManager.triggerOnNewPulse(
pulsePin, [this]() { mPulses++; });
}
double DirectionlessOdometer::getDistance() const
{
return mPulses == 0 ?
0.0 :
static_cast<double>(mPulsesPerMeter) / mPulses;
}
DirectionalOdometer::DirectionalOdometer(
int directionPin,
int pulsePin,
int pulsesPerMeter)
: DirectionlessOdometer(pulsePin, pulsesPerMeter)
, mDirectionPin{directionPin}
{
mInterruptManager.triggerOnNewPulse(
pulsePin,
[this]() {
mPinReader.read(mDirectionPin) ?
mPulses++ :
mPulses--;
});
}
Refactor
Abstract common functionality & inject
struct Encoder
{
virtual ~Encoder() = default;
virtual void incrementPulses() = 0;
virtual void decrementPulses() = 0;
virtual double getDistance() const = 0;
};
struct DirectionlessOdometer
{
DirectionlessOdometer(Encoder& encoder,
InterruptServiceManager& ism,
int pulsePin);
double getDistance() const;
private:
Encoder& mEncoder;
};
Abstract dependencies & inject
struct DirectionalOdometer
{
DirectionalOdometer(Encoder& encoder,
InterruptServiceManager& ism,
PinReader& pinReader,
int directionPin,
int pulsePin);
double getDistance() const;
private:
Encoder& mEncoder;
PinReader& mPinReader;
};
Time
Why is it difficult?
set(gtest_run_flags --gtest_repeat=1000)
How would you test this?
struct PowerController
{
PowerController(PinManager& pinManager,
InterruptManager& interruptManager);
bool turnOn();
private:
PinManager& mPinManager;
std::condition_variable mConditionVariable;
std::atomic<bool> mPulseReceived{false};
std::mutex mRunnerMutex;
};
bool PowerController::turnOn()
{
mPinManager.setPin(kPin);
std::this_thread::sleep_for(1s);
mPinManager.clearPin(kPin);
std::unique_lock<std::mutex> lk(mRunnerMutex);
mConditionVariable.wait_for(
lk,
10s,
[this]() { return mPulseReceived.load(); });
return mPulseReceived.load();
}
Refactor
struct TimeKeeper
{
virtual ~TimeKeeper() = default;
virtual void
sleepFor(std::chrono::milliseconds ms) const = 0;
};
struct AsynchronousTimer
{
virtual ~AsynchronousTimer() = default;
virtual void
schedule(std::function<void()> task,
std::chrono::seconds delay) = 0;
};
bool PowerController::turnOn() {
mPinManager.setPin(kPin);
mTimeKeeper.sleepFor(1s);
mPinManager.clearPin(kPin);
mAsynchronousTimer.schedule(
[this]() {
mPulseTimedOut = true;
mConditionVariable.notify_one(); }, 10s);
std::unique_lock<std::mutex> lk(mRunnerMutex);
mConditionVariable.wait(lk, [this]() {
return mPulseReceived.load() ||
mPulseTimedOut.load(); });
mPulseTimedOut = false;
return mPulseReceived.load();
}
Domain logic dependent
on application logic
Why is it difficult?
● "The dependency rule"
○ Clean architecture
● Circular dependencies
● Domain layer eventually becomes
unsustainable to develop or test
● (C++ specific) Macro madness
Variant A Variant B Variant C
Platform
How would you test this?
struct CommunicationManager
{
CommunicationManager(SerialPortClient& serial);
void sendViaSerial(std::string message);
private:
SerialPortClient& mSerialPortClient;
int mSequenceNumber{0};
};
void
CommunicationManager::sendViaSerial(std::string message)
{
#if defined(FOO_PRODUCT)
mSerialPortClient.send(
std::to_string(mSequenceNumber++) + ":" + message);
#elif defined(BAR_PRODUCT)
mSerialPortClient.send("M:" + message + ",");
#else
#error Did you forget to define a product?
#endif
}
Refactor
struct SerialFormatter {
virtual ~SerialFormatter() = default;
virtual std::string
format(std::string in) = 0;
};
std::string
BarSerialFormatter::format(std::string in) {
return "M:" + in + ",";
}
std::string
FooSerialFormatter::format(std::string in) {
return std::to_string(mSequenceNumber++) +
":" + input;
}
struct CommunicationManager {
CommunicationManager(
SerialPortClient& serialPortClient,
SerialFormatter& serialFormatter);
void sendViaSerial(std::string message);
private:
SerialPortClient& mSerialPortClient;
SerialFormatter& mSerialFormatter;
};
void
CommunicationManager::sendViaSerial(std::string message) {
mSerialPortClient.send(mSerialFormatter.format(message));
}
Refactoring workshop
● 19th of May
● Deeper look
● More patterns
○ Including the dreaded singleton
● Unit tests
● More Q&A
● Hands-on exercises
● 2 hours
● 399 SEK
Takeaways
More patterns & working code examples:
platisd/refactoring-for-testability-cpp
Contact me: dimitris@platis.solutions
● Unit tests should be atomic and run fast
● Verify your code (only)
● It should be fun
○ Not much up-front effort
● Abstract & inject
○ Care about what and not how
● Write SOLID code
● Follow OOP best practices
● Follow CPP core guidelines

Refactoring for testability c++

  • 1.
    Refactoring for testability inC++ Hard-to-test patterns in C++ and how to refactor them
  • 2.
    About me DimitriosPlatis ● Grew up in Rodos, Greece ● Software Engineer @ Edument, Gothenburg ● Course responsible @ Gothenburg University ● Interests: ○ Embedded systems ○ Software Architecture ○ API Design ○ Open source software & hardware ○ Robots, Portable gadgets, IoT ○ 3D printing ○ Autonomous Driving ● Website: https://platis.solutions
  • 5.
  • 6.
    Agenda 1. GoogleTest &GoogleMock 2. Ηard-to-test code 3. Easy-to-test code 4. Patterns to refactor
  • 7.
  • 8.
    The de facto(?) unit test framework for C++ ● xUnit test framework ● Runs on multiple platforms ● Open source ○ https://github.com/google/googletest ● Rich documentation & community ● Easy to integrate with CMake ● No fancy functionality ✔ Mocks, assertions, parameterized tests ✖ DI container, "spy"
  • 9.
  • 10.
    When is codehard to test? ● High complexity ● Tight coupling with other components ● Dependence on global/static states ● Sequential coupling ● ...more!
  • 11.
    When it's not fun ●Create fakes containing a lot of logic ● Change visibility of member functions and attributes ● Test things that have been tested before ● Sporadic fails ● Execution takes ages
  • 12.
  • 13.
    When is codeeasy to test? ● Low complexity ● SOLID ● Prefers composition over inheritance ● Abstracted dependencies ● Functional code without side-effects ● ...more!
  • 14.
  • 15.
    Grain of salt ●Treat these examples as generic guidelines ○ Won't necessarily apply to your project ● Maybe worse performance ○ 3 rules of optimization ● Possibly incompatible with your domain constraints ○ ISO 26262, AUTOSAR, MISRA ● Perhaps easier/cheaper with more advanced test frameworks ○ Fruit
  • 16.
    Core philosophy Getting own dependencies Managingown lifecycle Getting own configuration Ignorantly controlling self Dependencies injected Lifecycle managed from outside Configuration injected Inversion of Control Adopted from picocontainer.com article
  • 17.
  • 18.
    Why is itdifficult? bool write(const std::string& filePath, const std::string& content) { std::ofstream outfile(filePath.c_str(), std::ios::trunc); if (outfile.good()) { outfile << content << std::endl; } return outfile.good(); } std::optional<std::string> read(const std::string& filePath) { std::ifstream fileToRead(filePath); std::stringstream buffer; buffer << fileToRead.rdbuf(); if (buffer.good()) { return std::make_optional(buffer.str()); } return std::nullopt; }
  • 19.
    How would youtest this? bool FileEncoder::encode(const std::string& filePath) const { const auto validFileContents = read(filePath); if (!validFileContents) { return false; } auto encodedFileContents = validFileContents.value(); // Do something with file contents const auto wroteFileSuccessfully = write(filePath + ".encoded", encodedFileContents); return wroteFileSuccessfully; }
  • 20.
    Refactor Abstract struct FileReader { virtual~FileReader() = default; virtual std::optional<std::string> read(const std::string& filePath) const = 0; }; struct FileWriter { virtual ~FileWriter() = default; virtual bool write(const std::string& filePath, const std::string& content) const = 0; }; Inject struct FileEncoder { FileEncoder(FileReader& fileReader, FileWriter& fileWriter); bool encode(const std::string& filePath) const; private: FileReader& mFileReader; FileWriter& mFileWriter; };
  • 21.
  • 22.
    Why is itdifficult? struct DirectionlessOdometer { DirectionlessOdometer(int pulsePin, int pulsesPerMeter); double getDistance() const; protected: const int mPulsesPerMeter; int mPulses{0}; MyInterruptServiceManager mInterruptManager; }; struct DirectionalOdometer : public DirectionlessOdometer { DirectionalOdometer(int directionPin, int pulsePin, int pulsesPerMeter); private: MyPinReader mPinReader; const int mDirectionPin; };
  • 23.
    How would youtest this? DirectionlessOdometer::DirectionlessOdometer( int pulsePin, int pulsesPerMeter) : mPulsesPerMeter{pulsesPerMeter} { mInterruptManager.triggerOnNewPulse( pulsePin, [this]() { mPulses++; }); } double DirectionlessOdometer::getDistance() const { return mPulses == 0 ? 0.0 : static_cast<double>(mPulsesPerMeter) / mPulses; } DirectionalOdometer::DirectionalOdometer( int directionPin, int pulsePin, int pulsesPerMeter) : DirectionlessOdometer(pulsePin, pulsesPerMeter) , mDirectionPin{directionPin} { mInterruptManager.triggerOnNewPulse( pulsePin, [this]() { mPinReader.read(mDirectionPin) ? mPulses++ : mPulses--; }); }
  • 24.
    Refactor Abstract common functionality& inject struct Encoder { virtual ~Encoder() = default; virtual void incrementPulses() = 0; virtual void decrementPulses() = 0; virtual double getDistance() const = 0; }; struct DirectionlessOdometer { DirectionlessOdometer(Encoder& encoder, InterruptServiceManager& ism, int pulsePin); double getDistance() const; private: Encoder& mEncoder; }; Abstract dependencies & inject struct DirectionalOdometer { DirectionalOdometer(Encoder& encoder, InterruptServiceManager& ism, PinReader& pinReader, int directionPin, int pulsePin); double getDistance() const; private: Encoder& mEncoder; PinReader& mPinReader; };
  • 25.
  • 26.
    Why is itdifficult? set(gtest_run_flags --gtest_repeat=1000)
  • 27.
    How would youtest this? struct PowerController { PowerController(PinManager& pinManager, InterruptManager& interruptManager); bool turnOn(); private: PinManager& mPinManager; std::condition_variable mConditionVariable; std::atomic<bool> mPulseReceived{false}; std::mutex mRunnerMutex; }; bool PowerController::turnOn() { mPinManager.setPin(kPin); std::this_thread::sleep_for(1s); mPinManager.clearPin(kPin); std::unique_lock<std::mutex> lk(mRunnerMutex); mConditionVariable.wait_for( lk, 10s, [this]() { return mPulseReceived.load(); }); return mPulseReceived.load(); }
  • 28.
    Refactor struct TimeKeeper { virtual ~TimeKeeper()= default; virtual void sleepFor(std::chrono::milliseconds ms) const = 0; }; struct AsynchronousTimer { virtual ~AsynchronousTimer() = default; virtual void schedule(std::function<void()> task, std::chrono::seconds delay) = 0; }; bool PowerController::turnOn() { mPinManager.setPin(kPin); mTimeKeeper.sleepFor(1s); mPinManager.clearPin(kPin); mAsynchronousTimer.schedule( [this]() { mPulseTimedOut = true; mConditionVariable.notify_one(); }, 10s); std::unique_lock<std::mutex> lk(mRunnerMutex); mConditionVariable.wait(lk, [this]() { return mPulseReceived.load() || mPulseTimedOut.load(); }); mPulseTimedOut = false; return mPulseReceived.load(); }
  • 29.
    Domain logic dependent onapplication logic
  • 30.
    Why is itdifficult? ● "The dependency rule" ○ Clean architecture ● Circular dependencies ● Domain layer eventually becomes unsustainable to develop or test ● (C++ specific) Macro madness Variant A Variant B Variant C Platform
  • 31.
    How would youtest this? struct CommunicationManager { CommunicationManager(SerialPortClient& serial); void sendViaSerial(std::string message); private: SerialPortClient& mSerialPortClient; int mSequenceNumber{0}; }; void CommunicationManager::sendViaSerial(std::string message) { #if defined(FOO_PRODUCT) mSerialPortClient.send( std::to_string(mSequenceNumber++) + ":" + message); #elif defined(BAR_PRODUCT) mSerialPortClient.send("M:" + message + ","); #else #error Did you forget to define a product? #endif }
  • 32.
    Refactor struct SerialFormatter { virtual~SerialFormatter() = default; virtual std::string format(std::string in) = 0; }; std::string BarSerialFormatter::format(std::string in) { return "M:" + in + ","; } std::string FooSerialFormatter::format(std::string in) { return std::to_string(mSequenceNumber++) + ":" + input; } struct CommunicationManager { CommunicationManager( SerialPortClient& serialPortClient, SerialFormatter& serialFormatter); void sendViaSerial(std::string message); private: SerialPortClient& mSerialPortClient; SerialFormatter& mSerialFormatter; }; void CommunicationManager::sendViaSerial(std::string message) { mSerialPortClient.send(mSerialFormatter.format(message)); }
  • 33.
    Refactoring workshop ● 19thof May ● Deeper look ● More patterns ○ Including the dreaded singleton ● Unit tests ● More Q&A ● Hands-on exercises ● 2 hours ● 399 SEK
  • 34.
    Takeaways More patterns &working code examples: platisd/refactoring-for-testability-cpp Contact me: dimitris@platis.solutions ● Unit tests should be atomic and run fast ● Verify your code (only) ● It should be fun ○ Not much up-front effort ● Abstract & inject ○ Care about what and not how ● Write SOLID code ● Follow OOP best practices ● Follow CPP core guidelines