首页 > 解决方案 > 使用 Google 单元测试时如何在 C 中存根 fgets

问题描述

我目前被分配对我在介绍性训练营中完成的一些问题进行单元测试,但我在理解“存根”或“模拟”的概念时遇到了问题。

我正在使用谷歌单元测试,训练营中的问题在 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;
}

我应该如何思考/计划在 C 中对 fgets/malloc 之类的函数进行存根的过程?而且,如果不是太多,应该如何考虑测试这样的功能?

标签: cunit-testingfgets

解决方案


免责声明:这只是为 GoogleTest 模拟 C 函数的一种方法。当然还有其他方法。

模拟 C 函数的问题在于 GoogleTest 的工作方式。它所有很酷的功能都是基于派生一个 C++ 类来模拟和覆盖它的方法。这些方法也必须是虚拟的。但是 C 函数不是任何类的成员,仅是虚拟的。

我们找到并成功使用它的方式是提供一种包装类,其中包含与 C 函数具有相同原型的方法。此外,此类将指向自身实例的指针作为静态类变量。在某种意义上,这类似于单例模式,具有其所有特性,无论好坏。

每个测试都实例化这个类的一个对象,并使用这个对象进行公共检查。

最后,C 函数被实现为调用单个实例的同类方法的存根。


假设我们有这些 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

那么模拟类的头文件是:

// 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

这是模拟类的实现,包括替换 C 函数。所有函数都会检查单个实例是否存在。

// 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();
}

在非 void 函数上,您不能使用ASSERT_NE,因为它会以简单的return. 因此,对现有实例的检查更加详细。您也应该考虑一个好的默认值来返回。

现在我们开始编写一些测试。

// 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_...
}

编辑

我再次阅读了这个问题,并得出结论,需要一个更直接的例子。所以我们开始吧!我喜欢尽可能多地使用 Googletest 背后的魔法,因为它使扩展变得更加容易。在它周围工作感觉就像在反对它。

哦,我的系统是带有 MinGW64 的 Windows 10。

我是 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

这些 Makefile 可以轻松添加更多测试、模块、任何内容并记录所有选项。根据您的喜好扩展它。

测试模块

为了不得到警告,我不得不扩展提供的源:

// 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...

当然我们还需要一个头文件:

// module_to_test.h

#include <stdint.h>

int validate_input(uint32_t *input_value);

模拟课

模拟类是根据上面的示例建模的。启用“馈送”我添加参数化操作的字符串。

// 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

它的实现文件很简单,并提供了模拟的 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);
}

实施一些测试

以下是一些测试示例。不幸的是,要测试的模块使用stdout并且stderr捕获和测试并不那么简单。您可能想阅读“死亡测试”或提供您自己的重定向方法。在内核中,功能的设计并没有那么好,因为它没有考虑到测试。

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

构建和运行测试

这是我的(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

您会看到stderrC 函数的输出。

这是记录的日志,看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.

因为stdout它的输出与 Googletest 的输出混在一起。


推荐阅读