-
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.
MIPT-V / MIPT-MIPS — Cycle-accurate pre-silicon simulation.