friendship in service of testing -...
TRANSCRIPT
/ 47
Friendship in Service of Testing
Gábor Márton, [email protected]án Porkoláb, [email protected]
1
/ 47
Agenda
• Introduction, principals
• Case study (running example)
• Making the example better
• Vision
2
/ 47
Functional Programming
std::size_t fibonacci(std::size_t);ASSERT(fibonacci(0u) == 0u);ASSERT(fibonacci(1u) == 1u);ASSERT(fibonacci(2u) == 1u);ASSERT(fibonacci(3u) == 2u);...ASSERT(fibonacci(7u) == 13u);ASSERT(fibonacci(8u) == 21u);
• Immutability
• Thread safe
• Easy to test!
3
/ 47
OOP
• States
• Dependencies (e.g Strategy pattern)
• Bad patterns -> Side Effects (singleton)
4
/ 47
OOP - SOLID
• Loosely coupled system
• Interface (ptr or ref)
5
/ 47
UUT/SUT
dependencydependencyProduction
Test Double
MockStubFake
Often very heavy, e.g. requires network
Inter-face
Dependency Replacement - DR
Inter-face
6
/ 47
Creational Patterns• Factory Method
• Abstract Factory
• Service Locator
• Dependency Injection (DI)
• Extra Constructor / Setter
• DI != Dependency Replacement
7
/ 47
Creational Patterns• Factory Method
• Abstract Factory
• Service Locator
• Dependency Injection (DI)
• Extra Constructor / Setter
• DI != Dependency Replacement
• Who owns the object?
• Can we access the object from the test?
7
/ 47
OOP - Testing• Replace the dependency
• Access the dependency (private)
• To exercise them
• Make assertions on them
8
/ 47
Production
Often very heavy, e.g. requires network
UUT/SUT
dependencydependency
9
/ 47
Production
Often very heavy, e.g. requires network
No explicit interface (runtime or ct) What are the options for testing in C++?
UUT/SUT
dependencydependency
9
/ 4710
/ 47
• Linker
• Inline code?
• Header only code?
• Templates?
• Requires build system support
10
/ 47
• Linker
• Inline code?
• Header only code?
• Templates?
• Requires build system support
• Preprocessor
• namespaces?
10
/ 47
• Linker
• Inline code?
• Header only code?
• Templates?
• Requires build system support
• Preprocessor
• namespaces?
• Refactor
• Add that interface
10
/ 47
Seams
• Link seam
• Preprocessor seam
• Object seam
• Compile seam
With a seam we can alter behaviour in our program without editing the original unit under test.
11
/ 47
Seams
• Link seam
• Preprocessor seam
• Object seam
• Compile seam
With a seam we can alter behaviour in our program without editing the original unit under test.
The enabling point of a seam is the place where we can
make the decision to use one behaviour or another.
11
/ 47
class Entity {
public:
int process(int i) { return accumulate(v.begin(),v.end(),i); }
void add(int i) { v.push_back(i); }
private: vector<int> v;
}; void test1() { Entity e;
e.add(1); e.add(2);
ASSERT(e.process(0) == 3);
}
Case study
12
/ 47
class Entity {
public:
int process(int i) { return accumulate(v.begin(),v.end(),i); }
void add(int i) { v.push_back(i); }
private: vector<int> v;
}; void test1() { Entity e;
e.add(1); e.add(2);
ASSERT(e.process(0) == 3);
}
Case study
“Can we make it work in a multithreaded
environment?”
12
/ 47
class Entity {
public:
int process(int i) {
if(m.try_lock()) {
auto result = std::accumulate(...);
m.unlock();
return result; }
else { return -1; }
}
private:
std::mutex m; …};
13
/ 47
class Entity {
public:
int process(int i) {
if(m.try_lock()) {
auto result = std::accumulate(...);
m.unlock();
return result; }
else { return -1; }
}
private:
std::mutex m; …};
13
How to test the new logic?
/ 4714
class Entity {public: // Add a constructor // Doesn't compile and bad Entity(const std::mutex& m) : m(m) {} int process(int i) { if(m.try_lock()) { ... } else { ... } } ...private: std::mutex m; ...};
DR - Add a constructor
/ 4715
class Entity {public: // Let's assume that std::mutex is // copyable Entity(std::mutex m) : m(m) {} int process(int i) {...} ...private: std::mutex m; ...};
/ 4716
void test() { std::mutex m; // if std::mutex would be copyable Entity e(std::reference_wrapper<std::mutex>(m)); // set up m, that try_lock will fail set_try_lock_fails(m); // ??? ASSERT_EQUAL(m.process(1), -1);}
/ 47
struct Mutex { virtual void lock() = 0;
virtual void unlock() = 0;
virtual bool try_lock() = 0;
};
struct RealMutex : Mutex { … };
struct StubMutex : Mutex { bool try_lock_result = false;
virtual bool try_lock() override {
return try_lock_result;
} … };
17
/ 47
struct Mutex { virtual void lock() = 0;
virtual void unlock() = 0;
virtual bool try_lock() = 0;
};
struct RealMutex : Mutex { … };
struct StubMutex : Mutex { bool try_lock_result = false;
virtual bool try_lock() override {
return try_lock_result;
} … };
virtual ~Mutex() {}
17
/ 47
class Entity {
public:
Entity(Mutex& m) : m(m) {} int process(int i) { … }
private: Mutex& m;
};
Object Seam
18
/ 47
class Entity {
public:
Entity(Mutex& m) : m(m) {} int process(int i) { … }
private: Mutex& m;
};
• Ex. of Object Seam: Entity + Mutex • Enabling point: constructor
Object Seam
18
/ 47
void realWorldClient() {
RealMutex m;
Entity e(m);
// Real usage of e
}
void testClient() {
// test setup
StubMutex m;
Entity e(m);
m.try_lock_result = false;
ASSERT(e.process(1) == -1);
m.try_lock_result = true;
ASSERT(e.process(1) == 1);
}19
/ 47
class Entity {
public:
Entity(Mutex& m) : m(m) {} int process(int i) { … }
private: Mutex& m;
};
Feels so unnatural!
20
/ 47
class Entity {
public:
Entity(Mutex& m) : m(m) {} int process(int i) { … }
private: Mutex& m;
}; Ownership?
Feels so unnatural!
20
/ 47
class Entity {
public:
Entity(Mutex& m) : m(m) {} int process(int i) { … }
private: Mutex& m;
}; Ownership?Extra ctor/setter
Feels so unnatural!
20
/ 47
class Entity {
public:
Entity(Mutex& m) : m(m) {} int process(int i) { … }
private: Mutex& m;
}; Ownership?Extra ctor/setterExtra interface
Virtual functions Cache locality?
Feels so unnatural!
20
/ 47
class Entity {
public:
Entity(Mutex& m) : m(m) {} int process(int i) { … }
private: Mutex& m;
}; Ownership?Extra ctor/setterExtra interface
Virtual functions Cache locality?
Pointer semantics Cache locality?
Feels so unnatural!
20
/ 47
class Entity {
public:
Entity(Mutex& m) : m(m) {} int process(int i) { … }
private: Mutex& m;
}; Ownership?Extra ctor/setterExtra interface
Virtual functions Cache locality?
Pointer semantics Cache locality?
Type erasure? Can’t spare the virtual calls
Feels so unnatural!
20
/ 47
Ownershipclass Entity { public: Entity(std::unique_ptr<Mutex> m) : m(std::move(m)) {} int process(int i) { if(m−>try_lock()) { … } else { … }
} Mutex& getMutex() { return *m.get(); } private: std::unique_ptr<Mutex> m; }; void testClient() { Entity e(std::make_unique<StubMutex>()); auto& m = e.getMutex(); // assertions …
}21
/ 47
Ownership OK As before: Extra ctor, virtuals, pointer semantics, etc Extra getter
22
/ 47
template <typename Mutex> class Entity { public:
int process(int i) { … }
// Use only from tests Mutex& getMutex() { return m; }
private:
Mutex m;
};
Compile Seam
23
/ 47
void realWorldClient() { Entity<std::mutex> e; // Real usage of e
}void testClient() { struct StubMutex{ void lock() {} void unlock() {} bool try_lock_result = false; bool try_lock() { return try_lock_result; } }; Entity<StubMutex> e; auto& m = e.getMutex(); // assertions …
} 24
/ 47
void realWorldClient() { Entity<std::mutex> e; // Real usage of e
}void testClient() { struct StubMutex{ void lock() {} void unlock() {} bool try_lock_result = false; bool try_lock() { return try_lock_result; } }; Entity<StubMutex> e; auto& m = e.getMutex(); // assertions …
}
• No inheritance • No virtual functions
24
/ 47
Ownership
Locality
No Extra ctor/setter
Extra Interface (compile time)
Extra Getter
Extra Compilation Time
25
/ 47
Extra Compilation Time -Combine with Pimpl
// detail/Entity.hpp
namespace detail {
// as before
template <typename Mutex>class Entity { … };
} // detail
26
/ 47
// Entity.hppclass Entity {public: Entity(); ~Entity(); int process(int i);private: struct Impl; std::unique_ptr<Impl> pimpl;};
27
/ 47
// Entity.cpp
#include “detail/Entity.hpp” using RealEntity = detail::Entity<std::mutex>;struct Entity::Impl : RealEntity { // delegate the consturctors using RealEntity::RealEntity;};
Entity::Entity() : pimpl(std::make_unique<Impl>()) {}Entity::~Entity() = default;int Entity::process(int i) { return pimpl->process(i); } 28
/ 47
// Client.cpp#include “Entity.hpp”void realWorldClient() { Entity e; // Real usage of e}
// Test.cpp #include “detail/Entity.hpp”void testClient() { struct StubMutex { … }; detail::Entity<StubMutex> e; // Test code as before}
29
/ 47
No extra compile time
But at least two compiles if detail::Entity changes
Entity.cpp
Test.cpp
Extra complexity
Compile Seam + Pimpl
30
/ 47
template <typename Mutex> class Entity {public: Mutex& getMutex() { return m; } int process(int i) { … }private: Mutex m;};
class Entity {
public:
int process(int i) { … }
private:
std::mutex m;};
31
/ 47
template <typename Mutex> class Entity {public: Mutex& getMutex() { return m; } int process(int i) { … }private: Mutex m;};
class Entity {
public:
int process(int i) { … }
private:
std::mutex m;}; Intrusive
31
/ 47
Eliminate the Extra Gettertemplate <typename Mutex> class Entity { …#ifdef TEST Mutex& getMutex() { return m; }#endif …};
32
/ 47
Eliminate the Extra Gettertemplate <typename Mutex> class Entity { …#ifdef TEST Mutex& getMutex() { return m; }#endif …};
Worse
Readability
Maintainability32
/ 47
#define private public#include "Entity.hpp"#undef private// Test code comes from here
33
/ 47
#define private public#include "Entity.hpp"#undef private// Test code comes from here
Undefined behaviour
The order of allocation of non-static data members with different access control is unspecified
Danger, everything included from Entity.hpp is now public
class X {public: int x;private: int y;public: int z;};
xzy
xyz
34
/ 47
Eliminate the Extra Getter - Access via a Friend Function
template <typename Mutex> class Entity {public:+ friend Mutex& testFriend(Entity &e); int process(int i) { … }private: Mutex m;};
35
/ 47
// Test.cpp struct StubMutex { … };
StubMutex& testFriend(Entity<StubMutex>& e){ return e.m; }
void testClient() { Entity<StubMutex> e; auto &m = testFriend(e); // assertions …}
36
/ 47
Eliminate the Extra Getter - Access via a Friend Class
template <typename Mutex> class Entity {public:+ friend struct EntityTestFriend; int process(int i) { … }private: Mutex m;};
37
/ 47
// Test.cpp struct StubMutex { … };
struct EntityTestFriend { template<typename Mutex> static auto& getMutex(Entity<Mutex>& e) { return e.m; }}; void testClient() { Entity<StubMutex> e; auto &m = EntityTestFriend::getMutex(e); // assertions …}
38
/ 47
class EntityTestFriend { template<typename Mutex> static auto& getMutex(Entity<Mutex>& e) { return e.m; }+ friend void test_try_lock_succeeds();+ friend void test_try_lock_fails();}; void test_try_lock_succeeds() { Entity<StubMutex> e; auto &m = EntityTestFriend::getMutex(e); // assertion …}// …
39
/ 47
class EntityTestFriend { template<typename Mutex> static auto& getMutex(Entity<Mutex>& e) { return e.m; }+ friend void test_try_lock_succeeds();+ friend void test_try_lock_fails();}; void test_try_lock_succeeds() { Entity<StubMutex> e; auto &m = EntityTestFriend::getMutex(e); // assertion …}// … • Attorney <—> EntityTestFriend
• Client <—> Entity
39
/ 47
template <typename Mutex> class Entity {public: friend struct EntityTestFriend; int process(int i) { … }private: Mutex m;};
class Entity {
public:
int process(int i) { … }
private:
std::mutex m;};
40
/ 47
template <typename Mutex> class Entity {public: friend struct EntityTestFriend; int process(int i) { … }private: Mutex m;};
class Entity {
public:
int process(int i) { … }
private:
std::mutex m;}; Intrusive
40
/ 47
Eliminate the Extra Getter - Non-intrusive Access
template <typename Mutex> class Entity {public: int process(int i) { … }private: Mutex m;};
41
/ 47
// Test.cpp struct StubMutex { … };
ACCESS_PRIVATE_FIELD( Entity<StubMutex>, StubMutex, m)
void test_try_lock_fails() { Entity<StubMutex> e; auto& m = access_private::m(e); m.try_lock_result = false; ASSERT(e.process(1) == -1);}
42
/ 47
Access a private static member
class A { static int i;};int A::i = 42;
43
/ 47
Access a private static member
class A { static int i;};int A::i = 42;
template struct private_access<&A::i>;
43
/ 47
Access a private static member
class A { static int i;};int A::i = 42;
template struct private_access<&A::i>;
template <int* PtrValue> struct private_access { friend int* get() { return PtrValue; }};
43
/ 47
Access a private static member
class A { static int i;};int A::i = 42;
template struct private_access<&A::i>;
template <int* PtrValue> struct private_access { friend int* get() { return PtrValue; }};
int* get();
43
/ 47
Access a private static member
class A { static int i;};int A::i = 42;
template struct private_access<&A::i>;
template <int* PtrValue> struct private_access { friend int* get() { return PtrValue; }};
int* get();
void usage() { int* i = get(); assert(*i == 42);}
43
/ 47
Access a private non-static memberclass A { int i = 42;};
44
/ 47
Access a private non-static memberclass A { int i = 42;};
template struct private_access<&A::i>;
44
/ 47
Access a private non-static memberclass A { int i = 42;};
template struct private_access<&A::i>;
using PtrType = int A::*;template<PtrType PtrValue> struct private_access { friend PtrType get() { return PtrValue; }};
44
/ 47
Access a private non-static memberclass A { int i = 42;};
template struct private_access<&A::i>;
using PtrType = int A::*;template<PtrType PtrValue> struct private_access { friend PtrType get() { return PtrValue; }};
PtrType get();
44
/ 47
Access a private non-static memberclass A { int i = 42;};
template struct private_access<&A::i>;
using PtrType = int A::*;template<PtrType PtrValue> struct private_access { friend PtrType get() { return PtrValue; }};
PtrType get();
void usage() { A a; PtrType ip = get(); int& i = a.*ip; assert(i == 42);} 44
/ 47
Can access private member fields, functions
Can’t access private types
Can’t access private ctor/dtor
Link error with in-class defined private static const
45
/ 4746
/ 47
Eliminate the Extra Getter - Out-of-class Friend
template <typename Mutex> class Entity { public: int process(int i) { … } private: Mutex m;};
47
/ 47
Eliminate the Extra Getter - Out-of-class Friend
template <typename Mutex> class Entity { public: int process(int i) { … } private: Mutex m;};
// Test.cpp friend for(Entity<StubMutex>) void testClient() { Entity<StubMutex> e; auto& m = e.m; // access the private // assertions …}
47
/ 47
Clear intentions, self describing code
No cumbersome accessor patterns
Can access all private (types, ctor, …)
Proof-of-concept implementation in clang
Encapsulation (?)
A good language should prevent non-intuitive, accidental failure
“friend for” cannot be accidental
48
/ 47
Visionclass Entity { … };
49
/ 47
Visionclass Entity { … };
// Test.cpp void testClient() { using EntityUnderTest = test::ReplaceMemberType<Entity, std::mutex, StubMutex>; EntityUnderTest e; auto& m = e.get<StubMutex>(); // assertions … }
49
/ 47
Thank you!• Gábor Márton
• https://github.com/martong/access_private
• https://github.com/martong/clang/tree/out-of-class_friend_attr
• Questions?
50