简体   繁体   English

使用 Google 单元测试时如何在 C 中存根 fgets

[英]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.我正在使用 Google 单元测试,训练营中的问题在 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?我应该如何思考/计划在 C 中存根像 fgets/malloc 这样的函数的过程? 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.免责声明:这只是为 GoogleTest 模拟 C 函数的一种方式。 There are other methods for sure.当然还有其他方法。

The problem to mock C functions lays in the way GoogleTest works.模拟 C 函数的问题在于 GoogleTest 的工作方式。 All its cool functionality is based on deriving a C++ class to mock and overriding its methods.它所有很酷的功能都基于派生一个 C++ 类来模拟和覆盖其方法。 These methods must be virtual, too.这些方法也必须是虚拟的。 But C function are no members of any class, left alone of being virtual.但是 C 函数不是任何类的成员,只能是虚拟的。

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.我们发现并成功使用它的方式提供了一种包装类,其中包括与 C 函数具有相同原型的方法。 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.最后,C 函数被实现为调用相同类型的单个实例方法的存根。


Let's say we have these C functions:假设我们有这些 C 函数:

// 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.这是模拟类的实现,包括替换 C 函数。 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 .在非 void 函数上,您不能使用ASSERT_NE因为它会通过简单的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.我喜欢尽可能多地使用 Googletest 背后的魔法,因为它使扩展变得更加容易。 Working around it feels like working against it.解决它感觉就像反对它。

Oh, my system is Windows 10 with MinGW64.哦,我的系统是带有 MinGW64 的 Windows 10。

I'm a fan of Makefiles:我是 Makefile 的粉丝:

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.这些 Makefile 使添加更多测试、模块、任何内容和记录所有选项变得容易。 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.它的实现文件很简单,并提供了模拟的 C 函数。

// 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.不幸的是,模块到测试使用stdoutstderr ,它们不是那么容易捕获和测试。 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:这是我的(Windows)控制台在全新构建和测试时的输出:

> 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.您会看到 C 函数的stderr输出。

And this is the recorded log, see the Makefile how it is produced.这是记录的日志,看Makefile是如何产生的。

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.由于stdout上的stdout与 Googletest 的输出混淆。

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.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:如果测试者希望强制 fgets 返回 NULL:

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

    ASSERT_EQ(0,validate_input(&tester));

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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