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