-
Notifications
You must be signed in to change notification settings - Fork 137
Introduction to Test Driven Development
There is a small lesson on what TDD is.
We are going to look into simple class MIPSRegister, which holds identifier of MIPS register.
The idea is: instead of writing the class at first, we start from writing a tests on behavior what we expect. Prior to do that, we should understand what interfaces we want from this class. For MIPSRegister they may be:
- Deserialization: We expect any number between 0 and 31 to generate a valid object of MIPSRegister class
- Special objects: We expect that user can create object to represent HI, LO, HI_LO, RA, Zero, registers
- Serialization: We expect that objects may be converted to
std::size_t - Check: We expect to check properies of object (is HI, is LO, is RA, is Zero ?)
- Print: We expect objects to be outputed according to MIPS ISA
So, synopsys should follow:
class MIPSRegister {
public:
// Deserializing ctor
explicit MIPSRegister( uint8 value);
// Special objects
static const MIPSRegister mips_hi;
static const MIPSRegister mips_lo;
static const MIPSRegister mips_hi_lo;
static const MIPSRegister ra;
static const MIPSRegister zero;
// Serialization
const size_t to_size_t() const;
// Check
bool is_mips_hi() const;
bool is_mips_lo() const;
bool is_mips_hi_lo() const;
bool is_zero() const;
bool is_ra() const;
// Output
friend std::ostream& operator<<( std::ostream& out, const MIPSRegister& rhs);
};Now we write what we expect from this class to do. For instance:
- We expect any number NOT between 0 and 31 NOT to generate a valid object of MIPSRegister class
- We expect that objects generated from numbers do not represent a HI, LO or HI_LO register
- We expect that 0 generates Zero register and 31 generates return address
- We expect that serialization values of HI, LO, or HI_LO registers are not between 0 and 31 etc.
Such things can be easily expressed with GoogleTest environment:
TEST( MIPS_registers, Args_Of_Constr)
{
// Call a constructor
for ( size_t i = 0; i < 32; ++i)
{
GTEST_ASSERT_NO_DEATH( MIPSRegister reg( i); );
}
// Wrong parameter
ASSERT_EXIT( MIPSRegister reg( 32), ::testing::ExitedWithCode( EXIT_FAILURE), "ERROR: Invalid MIPS register id*");
}
What are the benefits? We cannot implement something wrong inside class MIPSRegister. Moreover, we may start from a drafty implementation and modify class internals later. For example, we may implement the output function in a silly manner
friend std::ostream& operator<<( std::ostream& out, const MIPSRegister& rhs)
{
switch( rhs.value)
{
case 0: return out << "zero";
case 1: return out << "at";
// ...
case 30: return out << "fp";
case 31: return out << "ra";
default: return out << "Null";
}
}and then do something more smart, like array generated from define file:
std::array<std::string_view, MIPSRegister::MAX_REG> MIPSRegister::regTable =
{{
#define REGISTER(X) # X
#include "mips_register.def"
#undef REGISTER
}};
friend std::ostream& operator<<( std::ostream& out, const MIPSRegister& rhs)
{
return out << regTable[ rhs.value];
}Now we can clone this test to and rename it to RISCVRegister as it should have same interfaces to satisfy RF. But, there are some changes on behavior level:
- We expect that no register is HI, LO or HI_LO
- We expect that registers generated by mips_hi, mips_lo, and mips_hi_lo are invalid and their usage will lead to error etc.
And after tests are ready, we start to write RISCVRegister implementation.
Let's start from a side note on OOP. Basically, class methods are divided into two groups:
- Getters — they return some values, but don't modify class.
auto get(/* args */) const;- Mutators — they return nothing, but modify class
void mutate(/* args */);For example, let's look at std::queue:
// getters
bool empty() const;
size_t size() const;
const value_type& front() const;
const value_type& back() const;
// mutators
void pop();
void push(const value_type&);
void emplace(/* arguments */);However, a lot of methods are 'mixed', due to bad design or complexity overhead
In MIPSRegister example we tested getters mostly. How to test mutators? It would be easy if we tested getters already. The idea is to use getters to ensure that mutation happened correctly. For example, we are testing std::queue:
std::queue<int> q;
q.push(2);
ASSERT_EQ(queue.front(), 2); // We expect to see 2 at front of the queue
ASSERT_EQ(queue.back(), 2); // We expect to see the same value at back
ASSERT_FALSE(queue.empty());
ASSERT_EQ(queue.size(), 1);
q.push(3)
ASSERT_EQ(queue.front(), 2); // We still expect to see 2 at front of the queue
ASSERT_EQ(queue.back(), 3); // We expect to see new value at back
ASSERT_FALSE(queue.empty());
ASSERT_EQ(queue.size(), 2);
q.pop();
ASSERT_EQ(queue.front(), 3); // There should be 3 at front of the queue
ASSERT_EQ(queue.back(), 3); // We expect to see 3 at back
ASSERT_FALSE(queue.empty());
ASSERT_EQ(queue.size(), 1);
q.pop();
ASSERT_TRUE(queue.empty()); // Must be empty now
ASSERT_EQ(queue.size(), 0); // Must have zero sizeMIPT-V / MIPT-MIPS — Cycle-accurate pre-silicon simulation.