cpputest icon indicating copy to clipboard operation
cpputest copied to clipboard

Parameterized tests

Open basvodde opened this issue 13 years ago • 10 comments

Should CppUTest support Parameterized tests better?

basvodde avatar Aug 11 '12 07:08 basvodde

Sounds intriguing.

kaos avatar Aug 13 '12 08:08 kaos

I'd be happy with an example of how to do it. Google can't seem to find one for me. :)

jbrains avatar Aug 27 '12 08:08 jbrains

@jbrains Uhm, well it isn't supported yet, so I'm a bit unclear what you mean with an example :)

basvodde avatar Sep 03 '12 07:09 basvodde

@basvodde Then I suppose that gives me my answer. I'm glad I didn't spend too long looking. :)

jbrains avatar Sep 03 '12 13:09 jbrains

@jbrains Eheh, yeah... what were you thinking. Asking for examples of features that don't exist :P It almost sounds like...

basvodde avatar Sep 04 '12 00:09 basvodde

Specification by Example :D

I have missed in actual work the ability to loop tests, e.g. run a test case with a loop counter.

@basvodde Is that by what you meant, by any chance?

e.g.

LOOP_TEST(GroupName, TestName, n, 0, 1, 100)
{
    dosomething(n);
    CHECK(some_condition);
}

arstrube avatar Jul 10 '14 12:07 arstrube

A prameterized test would be something like this:

TEST(x, y, z) { CHECK(do_stuff(y)) }

TEST_PARAMETERIZED(x, y, 1, 2, 3, 4, 5, 6)

Now it would create tests for all these values...

The above won't ever work, but it is something to think about :)

basvodde avatar Jul 10 '14 13:07 basvodde

There is a TestTestingFixture class implemented, but ... how shall I use it? So... there is an example what I like to do: I'd like to test a function what implements a simple state machine:

void periodicallyCalled_cookingStateMachine()
{
    static SimpleString currentMeal = "";
    static State state = Pending;
    static unsigned remainingTime = 0;

    switch (state) {
    case Pending:
        currentMeal = getNextOrder();
        if (!currentMeal.IsEmpty()) {
            remainingTime = getCookTime(currentMeal);
            if (remainingTime > 0) {
                startCooking();
                state = Cooking;
            } else {
                serve();
                state = Pending;
            }
        }
        break;
    case Cooking:
        if (remainingTime > PERIOD_TIME) {
            remainintTime -= PERIOD_TIME;
        } else {
            serve();
            state = Pending;
        }
        break;
    }
    reportCurrentState(state);
}

In the example above the required mocks are: getNextOrder, getCookTime, serve, reportCurrentState (startCooking is a local static function)

A well designed test of this state machine contains TestGroups for transitions grouped by start state. This is a good idea, because in this case the setup function can hold the required steps to reach the start state (it makes a possibility to differentiate failures by it comes from a prepare step or from the main transition). A TestCase (what tests a transition) contains TestFixtures for making boundary test:

/* Input and expected values */
static struct {
    bool expect_getNextOrder;
    SimpleString mealName;
    bool expect_getCookTime;
    unsigned cookTime;

    bool expect_serve;
    State state;
} parameters;

static void resetParameters()
{
    parameters.expect_getNextOrder = false;
    /* TODO: set mailName to a random one */
    parameters.expect_getCookTime = false;
    /* TODO: set cookTime to a random one */
    parameters.expect_serve = false;
    parameters.state = Pending;
}
static void setExpectations()
{
    if (parameters.expect_getNextOrder)
        mock().expectOneCall("getNextOrder").andReturnValue(parameters.mealName);
    if (parameters.expect_getCookTime)
        mock().expectOneCall("getCookTime").withStringParameter("mealName", parameters.mealName).andReturnValue(parameters.cookTime);

    if (parameters.expect_serve)
        mock().expectOneCall("serve").withStringParameter("mealName", parameters.mealName);
    mock().expectOneCall("reportCurrentState").withIntParameter("state", parameters.state); /* TODO: its an enum and not an int */
}

/* State transitions */
static void Step_PendingToCooking(const std::string &mealName, const unsigned &cookTime) {
    resetParameters();
    parameters.expect_getNextOrder = true;
    parameters.mealName = mealName;
    parameters.expect_getCookTime = true;
    parameters.cookTime = cookTime;
    parameters.state = Cooking;
    setExpectations();

    periodicallyCalled_cookingStateMachine();

    mock().checkExpectations();
}
static void Step_PendingToPending(const std::string &mealName) {
    resetParameters();
    parameters.expect_getNextOrder = true;
    parameters.mealName = mealName;
    parameters.expect_getCookTime = true;
    parameters.cookTime = 0;
    parameters.state = Pending;
    setExpectations();

    periodicallyCalled_cookingStateMachine();

    mock().checkExpectations();
}
static void Step_CookingToPending() {
//    /* Wait cook time */
//    unsigned waitCycles = parameters.cookTime / PERIOD_TIME;
//    for(unsigned i = 0; i < waitCycles; ++i) {
//        resetParameters();
//        parameters.state=Cooking;
//        setExpectations();
//
//        periodicallyCalled_cookingStateMachine();
//
//        mock().checkExpectations();
//    }

    /* The real transition */
    resetParameters();
    parameters.expect_serve=true;
    parameters.state=Pending;
    setExpectations();

    periodicallyCalled_cookingStateMachine();

    mock().checkExpectations();
}

TEST_GROUP(TransitionsFrom_Pending) {
    void setup() {}
    void teardown() { mock().clear(); }
};
TEST_GROUP(TransitionsFrom_Cooking) {
    void setup() {
        Step_PendingToCooking("myMeal", 1);
    }
    void teardown() { mock().clear(); }
};
TEST(TransitionsFrom_Pending, ToCooking) {
    Step_PendingToCooking("Boiled Egg", 1);
};
TEST(TransitionsFrom_Pending, ToPending) {
    Step_PendingToPending("Fresh Salad");
};
TEST(TransitionsFrom_Cooking, ToPending) {
    Step_CookingToPending();
}

Fixtures shall be used in test TransitionsFrom_Pending/ToCooking. I'd like to call Step_PreparedToCooking with different cook time (like 1, 0, UINT_MAX, PERIOD_TIME-1, PERIOD_TIME, PERIOD_TIME+1, ...). All of my test case use "parameters" as TestFixture, but with current framework there is no way to add multiple fixtures to a TEST. My proposal what looks similar than the current style of the framework:

TEST_WITH_FIXTURES(TransitionsFrom_Prepared, ToCooking)
{
    TEST_BODY() {
        unsigned cookTime = mock().getCurrentFixture().getUnsignedParam("cookTime");
        Step_PreparedToCooking(cookTime);
    }
    TEST_GENERATE_FIXTURES() {
        mock().getCurrentTest().addFixture("Normal").withUnsignedParam("cookTime", 1);
        mock().getCurrentTest().addFixture("Zero").withUnsignedParam("cookTime", 0);
        mock().getCurrentTest().addFixture("Max").withUnsignedParam("cookTime", UINT_MAX);
    }
};

And when a UTest(Shell) gets running, it shall iterate through internal fixtures (by TestFixture::_next member). (TEST_FIXTURES can be identified by a name, can be extended with "result shall be passed or failed"...)

There shall be a possibility to "reuse" a fixture. I mean when cooking time equals with PERIDO_TIME multiplied by 10, (I'd like to call "Stay_Cooking" 9th times with same fixture (parameters.state=Cooking) and after that once with a different one. To solve this issue, I think a wait loop shall be added to the beginning of Step_CookingToPending implementation - check commented lines. Do you have a better idea?)

Note: The code above contains a lot of compile errors/warnings I think, because I did not try to build it. Sorry.

CsTajbor avatar Jul 09 '20 09:07 CsTajbor

Should CppUTest support Parameterized tests better?

Yes please!

I'm used to have parametric tests in other frameworks/languages, for example:

martijnthe avatar Jul 22 '22 14:07 martijnthe