In this webinar, we talked about hard-to-test patterns in C++ and show how to refactor them. The difficulty, in this context, does not lie in the code's inherent complexity.
The focus will be on patterns technically difficult to unit test because they may:
* Require irrelevant software to be tested too
* E.g.: 3rd party libraries, classes other than the one under test
* Delay the test execution
* E.g.: sleeps inside code under test
* Require intricate structures to be copied or written from scratch
* E.g.: fakes containing a lot of logic
* Require test details to be included in the production code
* E.g.: #ifdef UNIT_TESTS
* Make changes and/or are dependent on the runtime environment
* E.g.: Creating or reading from files
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"
10. When is code hard 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
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
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
19. 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;
}
22. 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;
};
23. 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--;
});
}
30. 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
31. 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
}
33. 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
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