3

I have currently been assigned to do unit tests on some problems that I've done during an introductory bootcamp, and I'm having problems understanding the concept of 'stub' or 'mock'.

I'm using Google Unit Test, and the problems from the bootcamp are solved in C.

int validate_input(uint32_t * input_value) {

    char input_buffer[1024] = {0};
    char * endptr = NULL;
    int was_read_correctly = 1;

    printf("Give the value for which to print the bits: ");

    /* 
     * Presuming wrong input from user, it does not signal:
     * - number that exceeds the range of uint_32 (remains to be fixed)
     * For example: 4294967295 is the max value of uint_32 ( and this can be also confirmed by the output )
     * If bigger numbers are entered the actual value seems to reset ( go back to 0 and upwards.)
     */

    if (NULL == fgets(input_buffer, 1024, stdin)) {
        was_read_correctly = 0;
    } else {
        if ('-' == input_buffer[0]) {
            fprintf(stderr, "Negative number not allowed.\n");
            was_read_correctly = 0;
        }
    }

    errno = 0;

    if (1 == was_read_correctly) {
        * input_value = strtol(input_buffer, & endptr, 10);

        if (ERANGE == errno) {
            fprintf(stderr, "Sorry, this number is too small or too large.\n");
            was_read_correctly = 0;
        } else if (endptr == input_buffer) {
            fprintf(stderr, "Incorrect input.\n(Entered characters or characters and digits.)\n");
            was_read_correctly = 0;
        } else if ( * endptr && '\n' != * endptr) {
            fprintf(stderr, "Input didn't get wholely converted.\n(Entered digits and characters)\n");
            was_read_correctly = 0;
        }

    } else {
        fprintf(stderr, "Input was not read correctly.\n");
        was_read_correctly = 0;
    }

    return was_read_correctly;
}

How should I think/plan the process of stubbing a function like fgets/malloc in C? And, if it isn't too much, how a function like this should be thought to test?

ilya1725
  • 4,496
  • 7
  • 43
  • 68
  • A common definition: _A stub is a small piece of code that takes the place of another component during testing. The benefit of using a stub is that it returns consistent results, making the test easier to write. And you can run tests even if the other components are_ ***not working yet***. So, what about the code that you show is a problem specifically? What about it does ***not work*** the way you expect? – ryyker Aug 26 '20 at 12:40
  • The code is completely functional. I've just been assigned to learn how to unit test and been told to start practicing it on what I've done before. Now I'm having problems understanding the situations in which a stab is needed. For example, let's presume that the code is faulty and I'd like to know how to go on the if case in which fgets fails to read something, how would I do that? More specifically, how could I stub the fgets function? – Ionut-Alexandru Baltariu Aug 26 '20 at 12:42
  • _the code is faulty and I'd like to know how to go on the if case in which fgets fails to read something_ So, if by failing to read something, determine _why_ it failed to read something, and identify root cause. `fgets()` is part of the `C` standard, and for example can be the victim of undefined behavior, or can also be the victim of misuse by other methods. Ensure all of the component sections to your stub code are devoid of introducing UB, i.e. that they will propagate a predictable and well defined outcome. – ryyker Aug 26 '20 at 13:00
  • ...If when a particular set of inputs (as described above, with _no_ UB) results in unexpected outcome, you have identified a problem. you note it, document it, register a bug report and go on. – ryyker Aug 26 '20 at 13:01
  • Because this is perfectly functioning code, I think it would fit well as a question posted to [Code Review](https://codereview.stackexchange.com/), where questions of this kind are commonly asked. This forum is more focused on [questions as defined here](https://stackoverflow.com/help). I believe it might see a better response there. – ryyker Aug 26 '20 at 13:06
  • Here are a few examples of posts on this cite that ask more specific questions, and therefore were able to be answered within the parameters I linked above: [one](https://stackoverflow.com/questions/59394613/stub-system-function-in-google-test), [two](https://stackoverflow.com/questions/8959132/function-mocking-for-testing-in-c), [three](https://stackoverflow.com/questions/32131027/testing-conditions-that-exit-the-test) – ryyker Aug 26 '20 at 13:16
  • 2
    @ryyker I'm afraid that you misunderstood the issue of the OP. This is a question how to tackle the fact that Googletest is C++ and commonly uses derived classes to provide stubs and mocks. But `fgets()` and the function to test are C functions. -- Unfortunately I'm quite short on time currently, but I have done this. I might find some time tomorrow (in several hours) to post an answer. -- Ionut-Alexandru Baltariu, you might like to read all of Googletest's documentation in the meanwhile. There are some hints. – the busybee Aug 26 '20 at 17:38
  • @thebusybee - You could very well be right. Note, I did not ignore OP, and tried to help in terms of pointing in the right direction, but more than happy that you see it differently and may be able to offer some genuine help. Thanks for the comment. Once you post, please tag me in a comment again, I am interested in seeing how you address the issue! Thanks – ryyker Aug 26 '20 at 17:54
  • @thebusybee That is exactly what I wished to know as I'm having difficulties understanding how it could be done. I'll be patiently waiting for an answer, and, meanwhile, I'll be rereading the Google Unit Test Documentation. Thank you both guys for your answers. – Ionut-Alexandru Baltariu Aug 27 '20 at 04:18
  • I have managed to solve the problem. I have posted the solution in the answer section. Good luck to everybody and thanks for the answers! – Ionut-Alexandru Baltariu Aug 28 '20 at 08:15

2 Answers2

4

Disclaimer: This is just one way to mock C functions for GoogleTest. There are other methods for sure.

The problem to mock C functions lays in the way GoogleTest works. All its cool functionality is based on deriving a C++ class to mock and overriding its methods. These methods must be virtual, too. But C function are no members of any class, left alone of being virtual.

The way we found and use with success it to provide a kind of wrapper class that includes methods that have the same prototype as the C functions. Additionally this class holds a pointer to an instance of itself as a static class variable. In some sense this resembles the Singleton pattern, with all its characteristics, for good or bad.

Each test instantiates an object of this class and uses this object for the common checks.

Finally the C functions are implemented as stubs that call the single instance's method of the same kind.


Let's say we have these C functions:

// cfunction.h

#ifndef C_FUNCTION_H
#define C_FUNCTION_H

extern "C" void cf1(int p1, void* p2);

extern "C" int cf2(void);

#endif

Then the header file for the mocking class is:

// CFunctionMock.h

#ifndef C_FUNCTION_MOCK_H
#define C_FUNCTION_MOCK_H

#include "gmock/gmock.h"
#include "gtest/gtest.h"

#include "cfunction.h"

class CFunctionMock
{
public:
    static CFunctionMock* instance;

    CFunctionMock() {
        instance = this;
    }

    ~CFunctionMock() {
        instance = nullptr;
    }

    MOCK_METHOD(void, cf1, (int p1, void* p2));

    MOCK_METHOD(int, cf2, (void));

};

#endif

And this is the implementation of the mocking class, including the replacing C functions. All the functions check that the single instance exists.

// CFunctionMock.cpp

#include "CFunctionMock.h"

CFunctionMock* CFunctionMock::instance = nullptr;

extern "C" void cf1(int p1, void* p2) {
    ASSERT_NE(CFunctionMock::instance, nullptr);
    CFunctionMock::instance->cf1(p1, p2);
}

extern "C" int cf2(void) {
    if (CFunctionMock::instance == nullptr) {
        ADD_FAILURE() << "CFunctionMock::instance == nullptr";
        return 0;
    }

    return CFunctionMock::instance->cf2();
}

On non-void function you can't use ASSERT_NE because it quits on an error with a simple return. Therefore the check for an existing instance is a bit more elaborated. You should think of a good default value to return, too.

Now we get to write some test.

// SomeTest.cpp

#include "gmock/gmock.h"
#include "gtest/gtest.h"

using ::testing::_;
using ::testing::Return;

#include "CFunctionMock.h"

#include "module_to_test.h"

TEST(AGoodTestSuiteName, AndAGoodTestName) {
    CFunctionMock mock;

    EXPECT_CALL(mock, cf1(_, _))
        .Times(0);
    EXPECT_CALL(mock, cf2())
        .WillRepeatedly(Return(23));

    // any call of module_to_test that calls (or not) the C functions

    // any EXPECT_...
}

EDIT

I was reading the question once more and came to the conclusion that a more direct example is necessary. So here we go! I like to use as much of the magic behind Googletest because it makes extensions so much easier. Working around it feels like working against it.

Oh, my system is Windows 10 with MinGW64.

I'm a fan of Makefiles:

TESTS := Test

WARNINGLEVEL := -Wall -Wextra

CC := gcc
CFLAGS := $(WARNINGLEVEL) -g -O3

CXX := g++
CXXFLAGS := $(WARNINGLEVEL) -std=c++11 -g -O3 -pthread

LD := g++
LDFLAGS := $(WARNINGLEVEL) -g -pthread
LIBRARIES := -lgmock_main -lgtest -lgmock

GTESTFLAGS := --gtest_color=no --gtest_print_time=0

all: $(TESTS:%=%.exe)

run: all $(TESTS:%=%.log)

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

%.o: %.cpp
    $(CXX) $(CXXFLAGS) -I./include -c $< -o $@

%.exe: %.o
    $(LD) $(LDFLAGS) $^ -L./lib $(LIBRARIES) -o $@

%.log: %.exe
    $< $(GTESTFLAGS) > $@ || type $@

Test.exe: module_to_test.o FgetsMock.o

These Makefiles make it easy to add more tests, modules, anything, and document all options. Extend it to your liking.

Module to Test

To get no warning, I had to extend the provided source:

// module_to_test.c

#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

#include "module_to_test.h"

// all the rest is as in the OP's source...

And of course we need a header file:

// module_to_test.h

#include <stdint.h>

int validate_input(uint32_t *input_value);

The Mock Class

The mock class is modelled after the example above. Do enable "feeding" the string I added an parameterized action.

// FgetsMock.h

#ifndef FGETS_MOCK_H
#define FGETS_MOCK_H

#include <cstring>

#include "gmock/gmock.h"
#include "gtest/gtest.h"

ACTION_P(CopyFromSource, source)
{
    memcpy(arg0, source, arg1);
}

class FgetsMock
{
public:
    static FgetsMock* instance;

    FgetsMock()
    {
        instance = this;
    }

    ~FgetsMock()
    {
        instance = nullptr;
    }

    MOCK_METHOD(char*, fgets, (char*, int, FILE*));
};

#endif

Its implementation file is straight forward and provides the mocked C function.

// FgetsMock.cpp

#include <stdio.h>

#include "FgetsMock.h"

FgetsMock* FgetsMock::instance = nullptr;

extern "C" char* fgets(char* str, int num, FILE* stream)
{
    if (FgetsMock::instance == nullptr)
    {
        ADD_FAILURE() << "FgetsMock::instance == nullptr";
        return 0;
    }

    return FgetsMock::instance->fgets(str, num, stream);
}

Implementing Some Tests

Here are some examples for tests. Unfortunately the module-to-test uses stdout and stderr that are not so simple to catch and test. You might like to read about "death tests" or provide your own method of redirection. In the core, the design of the function is not that good, because it did not take testing into account.

// Test.cpp

#include "gmock/gmock.h"
#include "gtest/gtest.h"

using ::testing::_;
using ::testing::DoAll;
using ::testing::Ge;
using ::testing::NotNull;
using ::testing::Return;
using ::testing::ReturnArg;

#include "FgetsMock.h"

extern "C"
{
#include "module_to_test.h"
}

TEST(ValidateInput, CorrectInput)
{
    const char input[] = "42";
    const int input_length = sizeof input;
    FgetsMock mock;
    uint32_t number;

    EXPECT_CALL(mock, fgets(NotNull(), Ge(input_length), stdin))
        .WillOnce(DoAll(
            CopyFromSource(input),
            ReturnArg<0>()
        ));

    int result = validate_input(&number);

    EXPECT_EQ(result, 1);
    EXPECT_EQ(number, 42U);
}

TEST(ValidateInput, InputOutputError)
{
    FgetsMock mock;
    uint32_t dummy;

    EXPECT_CALL(mock, fgets(_, _, _))
        .WillOnce(Return(nullptr));

    int result = validate_input(&dummy);

    EXPECT_EQ(result, 0);
}

TEST(ValidateInput, NegativeInput)
{
    const char input[] = "-23";
    const int input_length = sizeof input;
    FgetsMock mock;
    uint32_t dummy;

    EXPECT_CALL(mock, fgets(NotNull(), Ge(input_length), stdin))
        .WillOnce(DoAll(
            CopyFromSource(input),
            ReturnArg<0>()
        ));

    int result = validate_input(&dummy);

    EXPECT_EQ(result, 0);
}

TEST(ValidateInput, RangeError)
{
    const char input[] = "12345678901";
    const int input_length = sizeof input;
    FgetsMock mock;
    uint32_t dummy;

    EXPECT_CALL(mock, fgets(NotNull(), Ge(input_length), stdin))
        .WillOnce(DoAll(
            CopyFromSource(input),
            ReturnArg<0>()
        ));

    int result = validate_input(&dummy);

    EXPECT_EQ(result, 0);
}

TEST(ValidateInput, CharacterError)
{
    const char input[] = "23fortytwo";
    const int input_length = sizeof input;
    FgetsMock mock;
    uint32_t dummy;

    EXPECT_CALL(mock, fgets(NotNull(), Ge(input_length), stdin))
        .WillOnce(DoAll(
            CopyFromSource(input),
            ReturnArg<0>()
        ));

    int result = validate_input(&dummy);

    EXPECT_EQ(result, 0);
}

Building and Running the Tests

This is the output of my (Windows) console when building freshly and testing:

> make run
gcc -Wall -Wextra -g -O3 -c module_to_test.c -o module_to_test.o
g++ -Wall -Wextra -std=c++11 -g -O3 -pthread -I./include -c FgetsMock.cpp -o FgetsMock.o
g++ -Wall -Wextra -std=c++11 -g -O3 -pthread -I./include -c Test.cpp -o Test.o
g++ -Wall -Wextra -g -pthread Test.o module_to_test.o FgetsMock.o -L./lib -lgmock_main -lgtest -lgmock -o Test.exe
Test.exe --gtest_color=no --gtest_print_time=0 > Test.log || type Test.log
Input was not read correctly.
Negative number not allowed.
Input was not read correctly.
Sorry, this number is too small or too large.
Input didn't get wholely converted.
(Entered digits and characters)
rm Test.o

You see the output of stderr of the C function.

And this is the recorded log, see the Makefile how it is produced.

Running main() from gmock_main.cc
[==========] Running 5 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 5 tests from ValidateInput
[ RUN      ] ValidateInput.CorrectInput
Give the value for which to print the bits: [       OK ] ValidateInput.CorrectInput
[ RUN      ] ValidateInput.InputOutputError
Give the value for which to print the bits: [       OK ] ValidateInput.InputOutputError
[ RUN      ] ValidateInput.NegativeInput
Give the value for which to print the bits: [       OK ] ValidateInput.NegativeInput
[ RUN      ] ValidateInput.RangeError
Give the value for which to print the bits: [       OK ] ValidateInput.RangeError
[ RUN      ] ValidateInput.CharacterError
Give the value for which to print the bits: [       OK ] ValidateInput.CharacterError
[----------] Global test environment tear-down
[==========] 5 tests from 1 test suite ran.
[  PASSED  ] 5 tests.

Because of the output on stdout it is mixed up with Googletest's output.

the busybee
  • 10,755
  • 3
  • 13
  • 30
0

I have managed to solve this issue in the following way:

header file for the stub function:

#ifndef STUBS_H_
#define STUBS_H_
    
#include "../src/p1.h"
    
char* fgets_stub(char *s, int size, FILE *stream);
    
#define fgets fgets_stub
    
#include "../src/p1.c"
    
char* fgets_RET;
   
#endif

implementation of stub function:

#include "stubs.h"

      
char* fgets_stub(char *s, int size, FILE *stream)
{
    if (NULL != fgets_RET)
    {
        strcpy(s,fgets_RET);
    }
    return fgets_RET;
}

how to test in test.cpp:

TEST(ValidateInput,CorrectionTest)
{
    uint32_t tester = 0;
    
    char* dummy_char = new char[NUM_OF_BITS];

    strcpy(dummy_char,"39131");

    cout<<dummy_char;

    fgets_RET = dummy_char;
    ASSERT_EQ(1,validate_input(&tester));

}

if the person that tests wishes to force NULL return of fgets:

TEST(ValidateInput,CorrectionTest)
{
    uint32_t tester = 0;
    
    fgets_RET = NULL;

    ASSERT_EQ(0,validate_input(&tester));

}