Testability vs Design
by Filip Sajdak
Who am I?
Automated tests are cool ...
but our code is untestable
What is Testability?
Software Testability
is the degree to which a software artifact supports testing in a given test context.
if is high, then finding faults in the system by means of testing is easier.
is not an intrinsic property of a software artifact and can not be measured directly
is an extrinsic property which results from interdependency of the software to be tested and the test goals, test methods used, and test resources
Determined by factors
- Controllability
- Observability
- Isolateability
- Separation of concerns
- Understandability
- Automatability
- Heterogeneity
What is Good Design?
Design considerations
- Compatibility
- Extensibility
- Modularity
- Fault-tolerance
- Maintainability
- Reliability
- Reusability
- Robustness
- Security
- Usability
- Performance
- Portability
- Scalability
Design considerations
- Compatibility
- Extensibility
- Modularity
- Fault-tolerance
- Maintainability
- Reliability
- Reusability
- Robustness
- Security
- Usability
- Performance
- Portability
- Scalability
Good Design Principles
-
Single responsibility principle
-
Open/closed principle
-
Liskov substitution principle
-
Interface segregation principle
-
Dependency inversion principle
Good Design Principles
-
Single responsibility principle
-
Open/closed principle
-
Liskov substitution principle
-
Interface segregation principle
-
Dependency inversion principle
Let's see some code
double pow(double a, int b) {
if (b<0)
return pow(1/a, -b);
double r = 1;
for (auto i = 0; i < b; ++i)
r *= a;
return r;
}
// ...
pow(5,3); // returns 125
pow(10, -3); // returns 0.001
Simple function
void print(double d)
{
std::cout << "<double>" << d << "</double>";
}
// ...
print(3.14);
More difficult one
void print(std::ostream& out, double d)
{
out << "<double>" << d << "</double>";
}
// ...
print(std::cout, 3.14);
What about this?
void print(std::ostream& out, double d, const Formater& f)
{
out << f(d);
}
// ...
print(std::cout, 3.14, XmlFormater{});
And what about this?
What design decision I have just made?
template <typename Formater>
void print(std::ostream& out, double d, Formater f)
{
out << f(d);
}
// ...
print(std::cout, 3.14, XmlFormater{});
What about this solution?
std::string formater(double d);
void print(std::ostream& out, double d)
{
out << formater(d);
}
// ...
print(std::cout, 3.14);
What about this solution?
void print(std::ostream& out, double d,
const Formater& f)
{
out << f(d);
}
template <typename Formater>
void print(std::ostream& out, double d, Formater f)
{
out << f(d);
}
void print(std::ostream& out, double d)
{
out << formater(d);
}
run-time
compile-time
link-time
What is a difference?
void print(std::ostream& out, double d, const Formater& f)
{
out << f(d);
}
void print(double d)
{
print(std::cout, d, XmlFormater{});
}
// ...
print(3.14);
If you want your previous API?
class Operation
{
public:
double operator()(double x, double y)
{
return 3*x + y;
}
};
Simple class
class Operation
{
public:
Operation(double c) : c{c} {}
double operator()(double x, double y)
{
return c*x + y;
}
private:
double c;
};
Simple class with initializer
class X
{
public:
void update()
{
auto state = d.getState();
s.save(state);
}
private:
Device d;
Store s;
};
Class with dependencies
class X
{
public:
X(std::shared_ptr<Device> d, std::shared_ptr<Store> s)
: d{d}, s{s} {}
void update()
{
auto state = d->getState();
s->save(state);
}
private:
std::shared_ptr<Device> d;
std::shared_ptr<Store> s;
};
Run-time dependency injection
// in cpp file
namespace {
DeviceMock* ptr = nullptr;
}
DeviceMock::DeviceMock() {
assert(ptr == nullptr);
ptr = this;
}
DeviceMock::~DeviceMock() {
ptr = nullptr;
}
State Device::getState() {
return ptr->getState();
}
Mocking in link time
class X
{
public:
void update()
{
auto st = d.getState();
s.save(st);
}
private:
Device d;
Store s;
};
struct DeviceMock {
DeviceMock();
~DeviceMock();
MOCK_METHOD0(getState, State());
};
template <typename DeviceType, typename StoreType>
class X {
public:
X(DeviceType& d, StoreType& s) : d{d}, s{s} {}
void update() {
auto state = d.getState();
s.save(state);
}
private:
DeviceType& d;
StoreType& s;
};
Compile time DI
template <typename DeviceType, typename StoreType>
class X {
public:
void update() {
auto state = d.getState();
s.save(state);
}
DeviceType& getDevice() { return d; }
StoreType& getStore() { return s; }
private:
DeviceType d;
StoreType s;
};
Compile time DI (2)
class X {
public:
template <typename T>
X(T d) : model_d(new model<T>(std::move(d))) {}
void update() { model_d->getState(); }
private:
struct imodel {
virtual ~imodel() = default;
virtual State getState() = 0;
};
template <typename T>
struct model : imodel {
T d;
model(T d) : d{std::move(d)) {}
State getState() { return d.getState(); }
};
std::unique_ptr<imodel> model_d;
};
Concept based polymorphism
Should we change a code to allow testing?
No - we should change a code to improve its design.
Takeaways
- avoid using global objects
- use abstractions but remember:
- there is not only run-time polymorphism
- it should improve your design
- know various ways of mocking your dependencies
- if you follow good design principles, you will increase testability of your code
Good design is testable, and design that isn’t testable is bad.
Michael Feathers
Working Effectively with Legacy Code
Hints
std::ostringstream local_stream;
auto prev_buf = std::cout.rdbuf();
std::cout.rdbuf(local_stream.rdbuf());
std::cout << "this will be written to local_stream instead of standard output";
std::cout.rdbuf(prev_buf);
How to test writing to cout?
template <typename DeviceType, typename StoreType>
class X {
public:
void update() {
auto state = d.getState();
s.save(state);
}
template <typename = decltype(d.gmock_getState())>
DeviceType& getDevice() { return d; }
template <typename = decltype(s.gmock_save(d.getState()))>
StoreType& getStore() { return s; }
private:
DeviceType d;
StoreType s;
};
Improve encapsulation
Design vs Testability
By Filip Sajdak
Design vs Testability
- 356