-2

I want to test-drive functions in C that manipulate an array. But the solutions that I find make me feel somewhat uncomfortable.

Lets speak about the limitations first: I'm using a forward declared struct that is a pointer to a kind of structure my test should have no knowledge of (only my implementation knows the actual details of the struct). That is in order to make sure users, I or my tests will only interact with the list using the to be programmed functions.

The reason that makes me feel uncomfortable about my solution is the following:

I create a first test that checks the list is empty on creation. I have a TaskList_Create() function. "Bad enough" the memory is already initialized with 0. So I can't really make a difference between memory as it was and it being initialized 0 by TaskList_Create().

All this leads me to creating another function without having a test for it. Namely TaskList_AddTask().

#include "TaskList.h"

TEST_GROUP(TaskList)
{
    TaskList tl;

    void setUp(void)
    {
        tl = TaskList_Create(4); /* 4 elements in size */
    }

    void tearDown(void)
    {
        TaskList_Destroy(tl);
    }
}

TEST(TaskList, Create_willInitializeListEmpty_v1)
{
    TaskList_AddTask(tl); /* returns non-zero value (PID) */
    TaskList_AddTask(tl); /* ... */
    TaskList_AddTask(tl);
    CHECK_EQUAL(4, TaskList_AddTask(tl));
    CHECK_EQUAL(0, TaskList_AddTask(tl));
}

TEST(TaskList, Create_willInitializeListEmpty_v2)
{
    CHECK_EQUAL(1, TaskList_IsEmpty(tl));
}

TEST(TaskList, Create_willInitializeListEmpty_v3)
{
    CHECK_EQUAL(0, TaskList_TasksInstalled(tl));
}

I use TaskList_AddTask() on the created list N+1 times, where N is the size of the list. This would allow me to verify the list is empty after TaskList_Create(), as the last call to TaskList_AddTask() would return some sort of failure (for which I have no test yet ... urg!).

The pattern that arises during this kind of development is that I will always have to create a untested function that will help me test the actual function I wanted to test in the first place.

I create TaskList_AddTask() to actually test TaskList_Create() or I write TaskList_IsEmpty().

Another issue: I want to test TaskList_DeleteTask(), which makes me write TaskList_AddTask().

TEST(TaskList, DeleteTask)
{
    uint8_t pid;
    TaskList_AddTask(tl);
    TaskList_AddTask(tl);
    TaskList_AddTask(tl);
    pid = TaskList_AddTask(tl);
    TaskList_DeleteTask(tl, pid);
    CHECK_EQUAL(4, TaskList_AddTask(tl));
}

Or: I want to test TaskList_Add() will find and recycle slots in the list that have previously been deleted, which forces me to write TaskList_DeleteTask().

In short: I don't feel comfortable having to write a 2nd function that is untested in order to test-drive another function.

I'm not exactly sure if faking/mocking would be of help her. Lets say, in order to test TaskList_DeleteTask() without having or using TaskList_AddTask() I'd have a FakeTaskList_Create() that I could pass a number that would specify the number of elements that should already be contained in the list, just in order to test my TaskList_DeleteTask() function.

I don't know too much about TDD-anti-patterns but what I'm doing right now sure feels like one.

Any, really any advice welcome!

FYI (but it should not matter):

typedef struct TaskStruct {
    void (*fp)(void);
    uint16_t periode;
    uint16_t delay;
    uint8_t run;
} TaskStruct;
typedef TaskList TaskStruct *;
4
  • You should not be creating functions just for testing. What would the caller expect to happen after tl = TaskList_Create(4)? Jun 27, 2017 at 12:53
  • Please show the definition of TaskList.
    – D Drmmr
    Jun 27, 2017 at 13:35
  • I can show you the TaskList struct, but the whole idea of a forward declared struct IMHO is that it is used as an abstract data type and no user can get or set anything on that data type without going through the functions that are intended to be used with it. I added the struct to the original post.
    – lazlo
    Jun 27, 2017 at 21:15
  • @lazlo I think you are right, your tests should call the code like a user would and assert the user's expectations. But it's not clear from your question how a user should call the code and what they should expect. Jun 27, 2017 at 22:03

2 Answers 2

1

Bad enough the memory is already initialized with 0. So I can't really make a difference between memory as it was and it being initialized 0 by TaskList_Create().

Typically, this will only be the case in a debug build. If you run your tests with full optimization, the memory will be left uninitialized after allocation.

The pattern that arises during this kind of development is that I will always have to create a untested function that will help me test the actual function I wanted to test in the first place.

Define pre- and post-conditions for each function and then write a test for each post-condition. You can then first test the 'basic' post-conditions of each function before testing their interaction.

0

The way I see it, your task functions perform changes on structs in memory. Ideally you would want to 'mock the memory' (analogous to how you would mock a database if your functions were operating on a database instead of a memory). Then you could go two ways, depending on how your mocks are constructed. You could either check the state (for example initialize your mocked memory to 0xff, and make sure after calling TaskList_CreateTask(), the first segment is 0x00. Or check that the right functions are called against the mock (memset(0) etc.).

I wish I could throw in some code examples, but I'm not a C expert (C# is my language of choice). The best I can do is direct you to this google search. There may be some C mock frameworks that will do exactly what you need.

6
  • "Mocking memory" would imply writing your own compiler. But the memory can be overwritten before the create function is called.
    – D Drmmr
    Jun 27, 2017 at 13:38
  • @DDrmmr You don't need to write your own compiler. Just make sure you don't call memory functions from your TaskList module directly. Add an indirection layer in-between, and mock that.
    – Eternal21
    Jun 27, 2017 at 16:36
  • That sounds like a really stupid idea to me. As a user of the class, I don't care how it handles memory. That's an implementation detail. Therefore it should not be part of the unit tests.
    – D Drmmr
    Jun 28, 2017 at 5:00
  • @DDrmmr Exactly - as a user of the class you don't care how it handles memory, you only care about the logic inherent to the Task Class. That's why you should test that logic in isolation, and the only way to isolate it from implementation (whether it's memory, or database, or file), is by mocking the layer below. That's how it's done the TDD way.
    – Eternal21
    Jun 28, 2017 at 11:30
  • If you don't care how memory is handled, then how can you "check that the right functions are called"?
    – D Drmmr
    Jun 28, 2017 at 13:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.