C++ Unit Tests With Boost.Test, Part 4

In Part 3 of this series, we completed Bob Martin’s Prime Factors Kata using test-driven development and unit tests written in C++ with Boost.Test. In this part, I will discuss test-driven UI development.

Test Driven Dialogs

One of the things that comes up often in discussions of test-driven development is the assertion that you can’t unit test UI behavior. Not true! Let’s enhance our PrimeFactors project with some user interface code. The requirements are:

Provide a dialog where the user is prompted for the value that will be passed to PrimeFactors::Generate. The dialog contains a text box and an OK button. Initially the OK button is disabled. As the user types each character in the text box, the OK button will be enabled as soon as the entered text is a valid integer.

OK, so how are we going to do this in test-driven development? We need to do this in a way that we can automate the test, but still capture the behavior of the dialog. The requirements are specifying the interaction between the controls on the dialog as events in the user interface occur. We know we want to stay away from instantiating the production dialog directly in our tests, because this will couple our test code to the user interface framework and might result in windows flashing up on the screen while the tests execute. Worse still, it may be impossible to create such a dialog from our UI framework without it blocking the execution of the test because the dialog is blocked waiting for input from a user.

Humble Dialogs and Mediators

What we need to do is make sure that all the behavior we want to create in response to tests is on some class other than the dialog. The dialog will delegate all its interesting behavior to this other class. This results in a humble dialog that does next to nothing and a mediator that has all the behavior. Our humble dialog will coordinate with the mediator through an interface defined by the mediator. This decouples the mediator from the dialog: all it knows are its own methods and the interface it uses to talk to its collaborating dialog. The dialog instantiates the mediator and passes itself as an implementor of the interface to the mediator.

First Failing Test: OKButtonInitiallyDisabled

Let’s start by writing our test for the mediator that says when the mediator is constructed, the first thing it does is disable the OK button. Since the lifetime of the mediator and the dialog are parallel, this means that the mediator will ensure the OK button is enabled when the dialog is first displayed.

Add a new header file to the PrimeFactors project called PrimeFactorsMediator.h and add the following code to the header:

#if !defined(PRIME_FACTORS_MEDIATOR_H)
#define PRIME_FACTORS_MEDIATOR_H

class IPrimeFactorsDialog
{
public:
    virtual ~IPrimeFactorsDialog() { }
    virtual void EnableOKButton(bool enabled) = 0;
};

class PrimeFactorsMediator
{
public:
    PrimeFactorsMediator(IPrimeFactorsDialog *dialog);
    ~PrimeFactorsMediator();
};

#endif

This defines the interface we need for this first test and describes the structure of the new class we’re going to create in a test-driven style. Now let’s write our test using this interface. Create a new source file in the Test project called TestPrimeFactorsMediator.cpp. Enter the following code into the file:

#include "stdafx.h"
#include "PrimeFactorsMediator.h"

BOOST_AUTO_TEST_CASE(OKButtonInitiallyDisabled)
{
    FakePrimeFactorsDialog dialog;
    PrimeFactorsMediator mediator(&dialog);
    BOOST_REQUIRE(dialog.EnableOKButtonCalled);
    BOOST_REQUIRE(!dialog.EnableOKButtonLastEnabled);
}

This is the essence of our test: we create a mediator and it should have called EnableOKButton on the supplied dialog. Furthermore, it should have called that method with an argument of false. Instead of using the real dialog for the test, we’re going to use a fake dialog for the test.

Fakes and Test Doubles

With the code entered above, the test will fail to compile because we haven’t defined FakePrimeFactorsDialog. What is this class doing? Remember that its the mediator that we are trying to test. However, the mediator needs to talk to the dialog through the IPrimeFactorsDialog interface. The purpose of the fake dialog class is to sense the interactions of the system under test (the mediator) with the dialog. The fake dialog is only used in our tests and not in our production code. In the production code, its the real dialog that will implement this interface and construct the mediator. That dialog could be implemented with MFC, raw Win32, wxWidgets or Qt. From the point of the view of the mediator, we don’t really care. All we care is that it implements the IPrimeFactorsDialog interface that we use to manipulate it. The fake dialog is a “test double” or stand-in for the real dialog.

Add the following code to the top of TestPrimeFactorsDialog.cpp to define the fake dialog:

class FakePrimeFactorsDialog : public IPrimeFactorsDialog
{
public:
    FakePrimeFactorsDialog()
        : EnableOKButtonCalled(false),
        EnableOKButtonLastEnabled(false)
    {
    }
    virtual ~FakePrimeFactorsDialog()
    {
    }
    bool EnableOKButtonCalled;
    bool EnableOKButtonLastEnabled;

    virtual void EnableOKButton(bool enabled)
    {
        EnableOKButtonCalled = true;
        EnableOKButtonLastEnabled = enabled;
    }
};

OK, that takes care of our fake dialog that senses how its called. Now we just need to stub out the implementation of the PrimeFactorsMediator class to get a failing test. Add a new source file called PrimeFactorsMediator.cpp to the PrimeFactors project and enter the following code:

#include "stdafx.h"

PrimeFactorsMediator::PrimeFactorsMediator(IPrimeFactorsDialog *dialog)
{
}

PrimeFactorsMediator::~PrimeFactorsMediator()
{
}

Build the solution and you should have code that compiles with a first failing test on the mediator.

First Passing Test: OKButtonInitiallyDisabled

To make the test pass, add the following code to the constructor of the PrimeFactorsMediator class:

dialog->EnableOKButton(false);

Build the solution and you should have a passing test. Whew! That took a while. However, we did quite a bit of design work just to sort out how to get that first test written. We defined an interface for a dialog, we created a mediator to orchestrate the controls on that dialog and we wrote a first test for that mediator.

Second Failing Test: OKButtonEnabledWithValidInteger

Now let’s write our second test. With the infrastructure we built for the first test, this should go more quickly. We want to write a test that says when the text box on the dialog contains a string representing a valid integer, then the OK button should be enabled. The requirements say that the validation should happen on a character-by-character basis, so we’ll need a way to invoke the mediator when the text changes. We’ll enhance the IPrimeFactorsDialog interface to provide a way for the mediator to obtain the current text in the value textbox. Add the following test case to TestPrimeFactorsMediator.cpp:

BOOST_AUTO_TEST_CASE(OKButtonEnabledWithValidInteger)
{
    FakePrimeFactorsDialog dialog;
    PrimeFactorsMediator mediator(&dialog);
    mediator.ValueTextChanged();
    BOOST_REQUIRE(dialog.EnableOKButtonCalled);
    BOOST_REQUIRE(dialog.EnableOKButtonLastEnabled);
}

Open PrimeFactorsMediator.h and include the standard C++ string header <string> and add the following method to the IPrimeFactorsDialog interface:

virtual std::string ValueText() = 0;

Since we’ve extended the interface, we need to add a stub implementation of this to our fake dialog. Add the following code to the FakePrimeFactorsDialog class in TestPrimeFactorsMediator.cpp:

virtual std::string ValueText()
{
    return "123";
}

Add the following method to the PrimeFactorsMediator class:

void ValueTextChanged();

Put an empty stub implementation of that method in PrimeFactorsMediator.cpp:

void PrimeFactorsMediator::ValueTextChanged()
{
}

Build the solution to obtain a failing test case.

Second Passing Test: OKButtonEnabledWithValidInteger

Now add just enough implementation to the mediator class to get the test to pass. We’ll need to access the dialog from ValueTextChanged, so we’ll need to remember it in the constructor. Add a member variable to the PrimeFactorsMediator class to hold the dialog interface in the header:

private:
    IPrimeFactorsDialog *m_dialog;

Change the constructor to remember the dialog interface that was passed in:

PrimeFactorsMediator::PrimeFactorsMediator(IPrimeFactorsDialog *dialog)
    : m_dialog(dialog)
{
    m_dialog->EnableOKButton(false);
}

Now add the following code to the body of PrimeFactorsMediator::ValueTextChanged:

m_dialog->EnableOKButton(true);

Now I know what you’re thinking: “he cheated!”. That’s right, I did. I did just enough implementation to make my test pass. Clearly this implementation doesn’t satisfy all the requirements because it always enables the OK button whenever the value text changes, regardless of the value. This is telling us that we haven’t yet written enough tests to cover the behavior. It is the opposite feeling we got when we wrote a new test case for PrimeFactors::Generate and it didn’t fail.

Third Failing Test: OKButtonDisabledWithEmptyText

OK, so we already have a feeling that our existing tests aren’t covering all the requirements. Let’s write another test that says when the value text changes and the textbox is empty, then the OK button is disabled. Add the following code to TestPrimeFactorsMediator.cpp:

BOOST_AUTO_TEST_CASE(OKButtonDisabledWithEmptyText)
{
    FakePrimeFactorsDialog dialog;
    PrimeFactorsMediator mediator(&dialog);
    mediator.ValueTextChanged();
    BOOST_REQUIRE(dialog.EnableOKButtonCalled);
    BOOST_REQUIRE(!dialog.EnableOKButtonLastEnabled);
}

OK, so now we have a new failing test.

Third Passing Test: OKButtonDisabledWithEmptyText

Currently our fake dialog is always returning “123” as the value text. Clearly we can’t have both OKButtonEnabledWithValidInteger and OKButtonDisabledWithEmptyText passing with this situation. So we need to enhance our fake dialog a little bit so that we can control what it will return for the result of ValueText. Add a bool member variable called ValueTextCalled and a std::string member variable called ValueTextFakeResult to FakePrimeFactorsDialog and initialize them appropriately in the construtor.

Change the implementation of ValueText to the following code:

virtual std::string ValueText()
{
    ValueTextCalled = true;
    return ValueTextFakeResult;
}

Now we can update the implementation of ValueTextChanged on the mediator to use the value from the dialog to decide whether or not to enable the OK button. Change ValueTextChanged to the following implementation:

void PrimeFactorsMediator::ValueTextChanged()
{
    m_dialog->EnableOKButton(m_dialog->ValueText().length() > 0);
}

If you build the solution, you’ll see that our new test case passes, but an existing test case OKButtonEnabledWithValidInteger now fails. Why is that? We made the fake dialog configurable and it no longer returns the same result for ValueText every time, so we need to enhance the setup portion of OKButtonEnabledWithValidInteger to properly configure the dialog. While we’re at it, we can enhance both tests to check that the value text was obtained from the dialog. Change the test to the following code:

BOOST_AUTO_TEST_CASE(OKButtonEnabledWithValidInteger)
{
    FakePrimeFactorsDialog dialog;
    dialog.ValueTextFakeResult = "123";
    PrimeFactorsMediator mediator(&dialog);
    mediator.ValueTextChanged();
    BOOST_REQUIRE(dialog.ValueTextCalled);
    BOOST_REQUIRE(dialog.EnableOKButtonCalled);
    BOOST_REQUIRE(dialog.EnableOKButtonLastEnabled);
}

Build the solution and obtain a set of passing tests.

Fourth Failing Test: OKDisabledWithInvalidIntegerText

OK, as you might have suspected, I cheated again! We still haven’t validated that the text is actually an integer. So let’s write a test where the OK button should be disabled when the value text is something that isn’t a valid integer. Add the following test to TestPrimeFactorsMediator.cpp:

BOOST_AUTO_TEST_CASE(OKDisabledWithInvalidIntegerText)
{
    FakePrimeFactorsDialog dialog;
    dialog.ValueTextFakeResult = "Invalid";
    PrimeFactorsMediator mediator(&dialog);
    mediator.ValueTextChanged();
    BOOST_REQUIRE(dialog.ValueTextCalled);
    BOOST_REQUIRE(dialog.EnableOKButtonCalled);
    BOOST_REQUIRE(!dialog.EnableOKButtonLastEnabled);
}

Build the solution to obtain a failing test case.

Fourth Passing Test: OKDisabledWithInvalidIntegerText

Now change the implementation of ValueTextChanged to make the test pass. This time, we can’t cheat anymore, we really do need to check that the text is a valid integer. Include the C++ standard header <sstream> to the top of the file and replace the implementation of ValueTextChanged with this code:

void PrimeFactorsMediator::ValueTextChanged()
{
    std::istringstream stream(m_dialog->ValueText());
    int value;
    stream >> value;
    m_dialog->EnableOKButton(!stream.fail() &&
        (m_dialog->ValueText().length() > 0));
}

Now all our tests are passing and we’ve implemented all the requirements of the dialog behavior.

Refactor!

With all our tests passing, now is a good time to refactor out some of the duplication in our tests. In each of the tests we have two repeated checks that determine whether or not the OK button was enabled or disabled. We can extract these two lines into a custom assertion on our fake dialog:

void AssertOKButtonEnabled(bool enabled)
{
    BOOST_REQUIRE(EnableOKButtonCalled);
    BOOST_REQUIRE_EQUAL(enabled, EnableOKButtonLastEnabled);
}

Once that has been extracted, we can update our tests to use the new custom assertion on the fake dialog. Here is how the last test looks after the refactoring:

BOOST_AUTO_TEST_CASE(OKDisabledWithInvalidIntegerText)
{
    FakePrimeFactorsDialog dialog;
    dialog.ValueTextFakeResult = "Invalid";
    PrimeFactorsMediator mediator(&dialog);
    mediator.ValueTextChanged();
    BOOST_REQUIRE(dialog.ValueTextCalled);
    dialog.AssertOKButtonEnabled(false);
}

Wiring Up The Production Dialog

The last part of this process is wiring up the mediator into the production dialog. How you do this is specific to the UI framework you’re using for your dialogs, but the general process is always the same:

  1. Include the header file for the mediator from the header file for the dialog.
  2. Add a member variable for the mediator to the dialog class. It usually can be instantiated directly and doesn’t need to be allocated from the heap with new.
  3. Implement the mediator’s dialog interface on the dialog class. As you can see from our interface, this usually consists of simple getter and setter methods that are easily mapped to the UI framework. It is important that these methods be easily implemented so that you don’t feel they need to be unit tested. (This would involve unit testing the dialog class itself, which is the thing we tried to avoid in the first place.)
  4. Initialize the mediator in the constructor for the dialog. The this pointer for the dialog would be passed to the mediator so that the mediator can communicate with the dialog. Depending on your compiler, this may generate a warning because the dialog hasn’t been fully constructed yet and any attempt to call a virtual method through the supplied this pointer may result in undefined behavior. In our case, we know that this is not going to happen, so we can ignore or suppress the warning in that particular case. An alternative is to define a two-phase initialization where the dialog is fully constructed and then a second method is called on the dialog to have it construct the mediator (with a fully functional this pointer) in the second phase. If you encapsulate construction of dialogs behind an object factory, this is a reasonable approach.

In Part 5 of this series, I will discuss the SetUp and TearDown facilities provided by Boost.Test that can be used to eliminate more of the duplication we have in the tests we wrote for the UI functionality.

[Updated to favor BOOST_REQUIRE over BOOST_CHECK.]

3 Responses to “C++ Unit Tests With Boost.Test, Part 4”

  1. C++ Unit Tests With Boost.Test, Part 3 « Legalize Adulthood! Says:

    […] me of follow-up comments via email. « C++ Unit Tests With Boost.Test, Part 2 C++ Unit Tests With Boost.Test, Part 4 » Blog at WordPress.com. • Theme: Garland by Steven Wittens and Stefan […]

    Like

  2. C++ Unit Tests With Boost.Test, Part 5 « Legalize Adulthood! Says:

    […] Unit Tests With Boost.Test, Part 5 5-July-2009 — legalize In Part 4 of this series, we completed some UI functionality using a test-driven development style. In this […]

    Like

  3. Two Sessions Accepted for C++ Now! 2014 (May 12-17, 2014) | Legalize Adulthood! Says:

    […] I wrote in 2009 on practicing test-driven development using Boost.Test: Part 1, Part 2, Part 3, Part 4 and Part 5. You can also work through the Tutorials in my Boost.Test documentation […]

    Like


Leave a comment