Fake News: Test-Driven Development has no Design!

Even if this was true, Test-Driven Development would be useful for realizing an up-front design. Humans make so many mistakes coding, the feedback loop of TDD helps you find those mistakes in the most efficient way I’ve ever seen (but I’ve only seen what I have seen). Certainly Test-First Programming (TFP) more effective than predominant programming practice, Debug Later Programming. TDD is much more than TFP. Let me debunk this fake news.

fake-news-banner

My First 2 decades of development

For about the first twenty years of my programing career, I pursued big design up-front (BDUF). Teams I led got pretty good with with BDUF. We would collect requirements for months on end. Get customers approval, then hide and do our design work, implement the system and go to the lab. We had specs, diagrams, text, pseudo code, and reviewed it all. It was an improvement over the chaos approach before BDUF. Our cycle time was an impressive 6 to 12 months (ahem). Sometimes we even made the deadline.

I worked mostly in embedded systems. When we finally got hardware, with anticipation we fired up the system somehow expecting it to work. Guess what? The system crashed. Now, late in the development cycle we had to deal with the mistakes, wrong requirements, untried hardware, unconfirmed design ideas and assumptions made to get to this point.

With reality setting in, we were forced to the iterative and incremental process we all know too well, debugging.

A Chage of Course in 1999

Thankfully my path changed in 1999. I attended the first Extreme Programming Immersion in the suburbs of Chicago at Object Mentor, my employer at the time. Kent and the crew described problems with development that I was all too familiar with: Late delivery, no warning of coming problems, defects delivered to customers, deteriorating design, and on top of that the burned out developers suffering through a death-march.

Kents 12 practices of XP seemed to address many of the problems. The techniques did not require a long explanations and a long time to payback. The logic-chain from problem to solution was made of only a few links. The discrete practices supported each other. We could try the ideas pretty easily and find out for ourselves.

I’d say at the time, XP was light on design advice. That was OK, because many of us early learners had already invested a lot in learning design. Robert Martin’s Object Mentor taught design and coined the term SOLID. We knew our way around upfront design.

XP does not prescribe how much design upfront is needed because each system has different needs. A small team needs less formal design than someone on a large multi-team effort. It is easy and ego building to over-engineer and design up-front. It is harder to do just enough design, just in time. We have to take a more humble attitude, we can’t get it all right up-front. Iterate to good design was a major insight by Kent, Ward, Ron, Martin and others that I am grateful to have heard about in 1999.

TDD Ecosystem

What experienced Test-Driven Developers simply call TDD today is more of an ecosystem than simply the application of one XP practice with a new name (the old name was TFP). Kent Beck originally used the term metaphor to describe design. It was confusing to me, but included having a system of names and conventions to navigate the design.

Recalling what Ron Jeffries told me back in 1999… design is so important, we do it everyday. When we discover something new about the need, the environment, or the existing system, consider the design impact. Design is not an isolated activity we can do once. Do design every day. Little things matter, and they scale. Ron said “Modularity works!”. TDD encourages modularity. TDD provides subtle pressure on a design to improve cohesion and coupling. These responsibility assignment choices happen at many levels everyday.

For me, design is creating a vision of the overall structure of the system. Design is never done, at least not until you stop supporting the system. Imagine a map of the United States, it’s made up of individual states connected through an interstate highway system. The states are also grouped into regions with different cultures, like Midwest, New England, the Southeast…. The country is sliced north to south into timezones. To navigate this system, diagrams, patterns and naming conventions really help. I want that help.

The TDD ecosystem includes applying design principles and having a vision of how the pieces are connected, how they are grouped and how they interact with each other. How formal does the design need to be? That depends on your situation.

Considering the needs continue to evolve, it is silly to think design happens only in the beginning. Nothing new here. We discovered this while we tried to do BDUF/waterfall in the late 80s. Did we give up on BDUF and waterfall? No. We started a number of little waterfalls. This helped, but we never would have had the insight to get to something like TDD and XP’s iterative and incremental engineering techniques.

Design is hard, learn from those before us

To be good at software development you need a lot of design insight and tools at your disposal. TDD is one of those tools. TDD helps you see your poor design decisions. Added complexity in tests, hard to use interfaces, complex initializations are some of the early warnings.

What are some of the other design principles and concepts the test-driven developer needs to understand and apply:

  • Principle of Least Knowledge.
  • Separation of Concerns (SoC)
  • SOLID
  • DRY
  • DAMP
  • GRASP
  • ZOMBIES
  • FIRST
  • DDD
  • BDD
  • Design Patterns
  • Hexagonal architecture (ports and adaptors)
  • Recognizing code smells
  • Envisioning a better design
  • Refactorings and incremental improvement
  • Law of Demeter (LoD)

Conclusion

There is plenty of design in the TDD ecosystem. Design is so important, do it all the time. BTW: you don’t have to learn TDD. Find another fast-feedback way to work. I would love to hear about it.

Here are a couple of other articles, from 2012 ,on the topic of TDD has no design.

“TDD ignores the design” “No it doesn’t” “Yes it does” – Part 1
“TDD ignores the design” “No it doesn’t” “Yes it does” – Part 2

Linker Substitution in C – Limitations and Workarounds

Let’s say you have code that in some test cases you want the real code and other test cases you want a test double. In C, using link-time binding, you only get to have a single function with a specific name.

I’d suggest that most the time this comes up you want to directly test some module (or function) and you want a substitute when testing code that depends on the module’s interface.

What options do you have?

  • Create multiple test runners, where one test runner would use the real function and the other test runner would use the test double.
  • Function pointer substitution.
  • #include the .c file in a namespace in the test file.
  • Linker wrapping.
  • Weak linking.

Multiple test runners

This might be a decent solution. Though it can lead to needing many test runners.

Function pointer substitution

Changing a direct function call to an indirect function call is a small change, and the compiler and linker will make sure you get it right. There are a couple examples in my book, in chapter nine. Here’s one of them.

// RandomMinute.h -- original
int RandomMinute_Get(void);
// RandomMinute.c -- original

int RandomMinute_Get(void)
{
    return bound - rand() % (bound * 2 + 1);
}
// RandomMinute.h - revised
extern int (*RandomMinute_Get)(void);
// RandomMinute.c - revised

static int __randomMinute_Get(void)
{
    return bound - rand() % (bound * 2 + 1);
}

int (*RandomMinute_Get)(void) = __randomMinute_Get;

That refactor is pretty easy, but you must make sure all users of RandomMinute_Get() include the header file. If you happen to be lazy about calling functions without having them advertised in a header file, the caller will think this is a direct function call. Bad things will happen.

In the test group file, write FakeRandomMinute_Get().

//Test file for SomethingThatUses_RandomMinute_Get

extern "C"
{
    static randomMinuteValue;
    static randomMinuteIncrement;
    static void FakeRandomMinute_Init(int first, int increment)
    {
        randomMinuteValue = first;
        randomMinuteIncrement = increment;
    }

    static int FakeRandomMinute_Get(void)
    {
        int value = randomMinuteValue;
        randomMinuteValue += randomMinuteIncrement;
        return value
    }
}

in setup() override RandomMinute_Get() with FakeRandomMinute_Get().

//Test file for SomethingThatUses_RandomMinute_Get - Continued

TEST_GROUP(SomethingThatUses_RandomMinute_Get)
{
    void setup()
    {
        //other setup
        FakeRandomMinute_Init(-30, 3);
        UT_PTR_SET(RandomMinute_Get, FakeRandomMinute_Get);
    }
}
    void teardown()
    {
    }
}

UT_PTR_SET overrides the function pointer and then restores its original value after teardown(). Your code under test now has predcitable random minutes.

In another test group, the production RamdomMinute_Get() function can be tested without overriding the function pointer.

// RandomMinuteTest.cpp

TEST_GROUP(RandomMinute) {}

TEST(RandomMinute, GetIsInRange)
{
    for (int i = 0; i < 100; i++)
    {
        minute = RandomMinute_Get();
        AssertMinuteIsInRange();
    }
}

But maybe you don’t want to use a function pointer. You might prefer this next solution.

#include the .c file in a C++ namespace

In this solution, the test-double’s function name in FakeRandomMinute.c is the same as the function being faked, business as usual for link-time substitution.

// FakeRandomMinute.h
#include "RandomMinute.h"
void FakeRandomMinute_Init(int first, int increment);
// FakeRandomMinute.c
#include "FakeRandomMinute.h"

void FakeRandomMinute_Init(int first, int increment)
{
    randomMinuteValue = first;
    randomMinuteIncrement = increment;
}

int RandomMinute_Get(void)
{
    int value = randomMinuteValue;
    randomMinuteValue += randomMinuteIncrement;
    return value
}

To test the production implementation, the the RandomMinute.c file can be #included in the test file. In addition, the #include needs to be wrapped in a C++ namespace to keep the name out of the global namespace.

// in your test file

namespace Test
{
    #include "RandomMinute.c"
}

In the test cases, make calls to Test::RandomMinute_Get().

TEST(RandomMinute, GetIsInRange)
{
    for (int i = 0; i < 100; i++)
    {
        minute = Test::RandomMinute_Get();
        AssertMinuteIsInRange();
    }
}

Linker Wrapping

The linker tricks are not generally available except from gcc. I’ve experimented with wrapping in gcc., but could not find it in visual studio or IAR (at the time I looked).

In gcc, wrap RamdomMinute_Get() with these command line options:

gcc -Wl,--wrap=RamdomMinute_Get ...

The linker will redirect calls to RamdomMinute_Get() to __wrap_RamdomMinute_Get(). You can get at there production implementation by calling __real_RamdomMinute_Get(). You need to provide __wrap_RamdomMinute_Get() to hold your fake implimentation and to keep the linker happy.

int __wrap_RamdomMinute_Get()
{
    // some fake implementation
}

Maybe some test cases want the real RamdomMinute_Get(), you could make the wrapped function default to the real with function.

static int (*customRamdomMinute_Get)() = 0;
int __wrap_RamdomMinute_Get()
{
    if (customRamdomMinute_Get)
            return customRamdomMinute_Get();
    return __real_RamdomMinute_Get();
}

I chose to use a function pointer for test specific behavior. This lets you have a generic fake implemention that is customized close to your test case.

There are some limitations so you may want to look at this Stack Overflow article for linker wrapping

Weak Linking

I’d say stay away from weak linking as it means adding non-standard stuff to your producion C code.

Finally

You can mix and match these techniques. Let me know how this works for you or if you know some other solutions to this problem.