Skip to content

Introduction to Test Driven Development

Pavel I. Kryukov edited this page Jul 25, 2018 · 5 revisions

In this lesson we implement class MIPSRegister, which holds identifier of MIPS register, using Test Driven Development (TDD).

Part 1: interfaces

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:

  1. Deserialization: We expect any number between 0 and 31 to generate a valid object of MIPSRegister class
  2. Special objects: We expect that user can create object to represent HI, LO, HI_LO, RA, Zero, registers
  3. Serialization: We expect that objects may be converted to std::size_t
  4. Check: We expect to check properies of object (is HI, is LO, is RA, is Zero ?)
  5. 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);
};

Part 2: Behavior

Now we write what we expect from this class to do. For instance:

  1. We expect any number NOT between 0 and 31 NOT to generate a valid object of MIPSRegister class
  2. We expect that objects generated from numbers do not represent a HI, LO or HI_LO register
  3. We expect that 0 generates Zero register and 31 generates return address
  4. 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 Catch2 environment:

TEST_CASE( "MIPS_registers: Equal")
{
    for ( size_t i = 0; i < 32; ++i)
    {
        CHECK(MIPSRegister(i) == MIPSRegister(i));
        if (i > 0) {
            CHECK(MIPSRegister(i - 1) != MIPSRegister(i));
        }
    }
}

TEST_CASE( "MIPS_registers: Hi_Lo_impossible")
{
    for ( size_t i = 0; i < 32; ++i)
    {
        MIPSRegister reg(i);
        CHECK_FALSE(reg.is_mips_hi());
        CHECK_FALSE(reg.is_mips_lo());
    }
}

TEST_CASE( "MIPS_registers: Zero")
{
    auto reg = MIPSRegister::zero;
    CHECK(reg.is_zero());
    CHECK_FALSE(reg.is_mips_hi());
    CHECK_FALSE(reg.is_mips_lo());
    CHECK(reg.to_size_t() == 0);
}

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];
    }

Epilogue

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:

  1. We expect that no register is HI, LO or HI_LO
  2. 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.

Clone this wiki locally