简体   繁体   中英

how to stub fgets in C while using Google Unit Test

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?

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.

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));

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM