2

How can I write a test in Ceedling for a function that uses static global variable? I would like to test for each possible value of the variable to achieve a good test coverage.

//Pseudo code for file_under_test.c
static int global_var;

int func_under_test(){ 

   switch(global_var){
    case x:
      return some_value;
    case y:
      return some_other_value;
    .
    .
    .
    .
    default:
      return something; 
   }

}
Nitesh
  • 31
  • 1
  • 5
  • I think this question highlights two problems worth discussing: 1) That using hidden state makes it harder to test something, and 2) "Test one function at a time" does not (always) work. You need to both be able to stimulate and to observe behaviour of the unit under test, and in this case you are missing a way to stimulate it. I'm assuming you have other functions in the same file which modifies `global_var` which you can use? – Joskar May 10 '20 at 13:04
  • @Joskar, you are right I do infact have a setter function in the same file. Now, I understand that the func_under_test cannot be tested in complete isolation and infact needs to be tested together with the setter function. However, I still do not understand how I can control the output of the setter function so the func_under_test can be stimulated with different values of the global_var. Can I mock this setter function in CMock? I have been doing some reading and it seems like CMock cannot mock a function that exists in the same file. – Nitesh May 11 '20 at 06:01
  • No, it is correct that you cannot mock a function which exists in the same translation unit as a non-mocked function. How you can control the hidden state depends case-by-case on how your "setter function" looks. If the setter is 1-to-1 with `global_var` already, then a simple `setter_function(x); int ret = func_under_test(); TEST_ASSERT_EQUAL(some_value, ret);` in your test would do. If the logic of your setter is more complex, then you need to adjust accordingly (with more calls to the setter in a certain order or whatever). But the latter is hard to answer without an example. – Joskar May 11 '20 at 06:50
  • There is an array declared in a different header file. Setter takes an array_index as an argument, reads the value at the given array_index and returns this value as global_var. – Nitesh May 11 '20 at 07:59

4 Answers4

1

This is a super common problem in unit testing C code and the most common solution I know of is to define the static keyword out of existence when testing. This requires some planning and is hard to do in legacy code but any static that I plan on testing against is replaced by some other string. Usually STATIC or better yet TESTABLE_STATIC.

Remember that ceedling and probable most unit test frameworks sets a compile time macro TEST so you code would be

//Pseudo code for file_under_test.c
#ifdef TEST
#define TESTABLE_STATIC 
#else
#define TESTABLE_STATIC static
#endif


TESTABLE_STATIC int global_var;

int func_under_test(){ 

   switch(global_var){
    case x:
      return some_value;
    case y:
      return some_other_value;
    .
    .
    .
    .
    default:
      return something; 
   }

}

Then in your test file you just treat the variable as a global

// your ceedling test 
#include <your_functions.h>

extern int global_var;
void test_function_under_test(void)
{
    // your test code setting global_var as needed
    global_var = some_test_value;
    TEST_ASSERT_EQUAL(expected_val, func_under_test());
}

I usually hide the TESTABLE_STATIC in a project header file or if you have a datatypes.h file so it is generally available everywhere in my project.

This also works for unit testing your static functions in a translation unit.

bd2357
  • 704
  • 9
  • 17
0

This question does not really have anything specifically to do with Ceedling (or Unity, CMock, etc), but I rather think this is an example of interpreting the word "unit" in a very specific way. The short version of my answer is that the example function you have written here does not really constitute a self-contained "unit", so I would claim that this is not really "unit-testable".

Only think of a "function" as a "unit" if it is a pure function or if you can find an appropriate seam (e.g. stubs, spies, or mocks to external interfaces)! Otherwise you would by neccessity need to check for implementation details inside the test, which makes for very brittle tests.

In order to have a "unit" of testable code, you need to both be able to see the effects of the unit under test (e.g. comparing the returned value and/or checking for other side effects) AND to be able to stimulate the unit under test (e.g. by passing arguments into a the function or by first setting up some side effects which the unit under test relies on).

The example function is relying on the side effects of some other function (one which has the side effect of modifying your static "global" variable), so a "proper unit" in this case would need include whatever function you have which triggers these side effects. I suppose your file already has at least one such function, or your example code would never return anything different*.

*This is unless your example actually has the side effect of modifying the static variable itself. In that case there should at least be a function which resets the "global state", otherwise your tests will not be isolated from each other (i.e. it is hard to make them order-independent). A better solution would be to explicitly expose the dependency of your state through the arguments to func_under_test, like func_under_test(struct some_opaque_type *state) and add a struct some_opaque_type *init_for_func_under_test() function.

TL;DR: If your function is not a pure function (e.g. it relies on hidden state and/or has side effects itself) or if you don't have the appropriate "seams" (e.g. stubs or spies), then also include functions which can modify the hidden state or verify the side effects in your definition of unit under test.

Joskar
  • 600
  • 5
  • 12
0

I used a wrapper, that includes the orginal C file and adds some helpers for test So you have an unchanged orginal c source, but access to all the needed internals.

file wrapped_for_test.h

#include <file_under_test.h>
void set_for_test(int value);

file wrapped_for_test.c

#include <file_under_test.c>

void set_for_test(int value)
{
  global_var = value;
}
notan
  • 359
  • 1
  • 10
-1

You can create a test helper function in file_under_test.c that will setup the global_var before calling func_under_test(). If needed this test function helper can be compiled only for testing purpose (using specific #ifdef) so that is it not shipped with the rest of the code if your code is build for eg a product.

file_under_test.h:

void set_for_test(int value);

file_under_test.c

#ifdef TESTS
void set_for_test(int value)
{
  global_var = value;
}
#endif

test_file.c:

#include <assert.h>
#include "file_under_test.h"

// some other tests
set_for_test(3);
assert (func_under_test() == something);
//...
Jean-Marc Volle
  • 3,113
  • 1
  • 16
  • 20
  • 1
    While I agree this would work for "throwaway" tests that aren't going to be checked in anywhere, adding functions to the code which pokes directly at implementation details and which only exists for the test code itself will most likely bite back in the future. It would be better if we could find a `set_for_test()`-like function which has a more "valid" use case outside the testing world, but which still could be used from within the test. – Joskar May 10 '20 at 12:48
  • Sorry but I strongly disagree ;-). The fact the code uses a static variable with no setter is an implementation choice (that can be debated for sure!). Writing the code so that it can be tested is a must, including accessing/checking some implementation details. This is one of the typical usage of friend functions/classes in C++. – Jean-Marc Volle May 10 '20 at 14:31
  • It is okay to disagree! I simply think that code should be tested through its public API whenever possible (otherwise it can become very hard to refactor with confidence). And using friend functions/classes does not seem to be a recommended practice either, although I know differing opinions exist on that topic: https://stackoverflow.com/a/4171331/688896 – Joskar May 10 '20 at 15:10