【问题标题】:how to stub fgets in C while using Google Unit Test使用 Google 单元测试时如何在 C 中存根 fgets
【发布时间】:2021-06-20 17:33:24
【问题描述】:

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

我正在使用 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;
}

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

【问题讨论】:

  • 一个常见的定义:存根是一小段代码,在测试期间代替另一个组件。使用存根的好处是它返回一致的结果,使测试更容易编写。即使其他组件 还没有工作,您也可以运行测试。那么,您显示的代码具体是一个问题呢?怎么样不工作你期望的方式?
  • 代码功能齐全。我刚刚被分配学习如何进行单元测试,并被告知要开始按照我以前做过的事情来练习它。现在我在理解需要刺伤的情况时遇到了问题。例如,假设代码有问题,我想知道如何处理 fgets 无法读取某些内容的 if 情况,我该怎么做?更具体地说,我如何存根 fgets 函数?
  • 代码有问题,我想知道如何处理 fgets 无法读取某些内容的 if 情况 因此,如果无法读取某些内容,请确定为什么它无法读取某些内容并确定根本原因。 fgets()C 标准的一部分,例如可能是未定义行为的受害者,也可能是其他方法滥用的受害者。确保存根代码的所有组件部分都没有引入 UB,即它们将传播可预测且定义明确的结果。
  • ...如果当一组特定的输入(如上所述,使用 no UB)导致意外结果时,您已发现问题。你记下它,记录它,注册一个错误报告并继续。
  • @ryyker 恐怕您误解了 OP 的问题。这是一个如何解决 Googletest 是 C++ 并且通常使用派生类来提供存根和模拟这一事实的问题。但是fgets() 和要测试的函数是 C 函数。 -- 不幸的是,我目前的时间很短,但我已经做到了。我可能会在明天(几个小时后)找到一些时间来发布答案。 -- Ionut-Alexandru Baltariu,您可能想同时阅读所有 Googletest 的文档。有一些提示。

标签: c unit-testing fgets


【解决方案1】:

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

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

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

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

最后,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);
}

实施一些测试

以下是一些测试示例。不幸的是,要测试的模块使用了stdoutstderr,它们不容易捕获和测试。您可能想阅读“死亡测试”或提供您自己的重定向方法。在内核中,函数的设计不是很好,因为它没有考虑到测试。

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

您会看到 C 函数的 stderr 的输出。

这是记录的日志,看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 的输出混淆。

【讨论】:

  • @ryyker 你来了!玩得开心!
【解决方案2】:

我已经设法通过以下方式解决了这个问题:

存根函数的头文件:

#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

存根函数的实现:

#include "stubs.h"

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

如何在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));

}

如果测试者希望强制 fgets 返回 NULL:

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

    ASSERT_EQ(0,validate_input(&tester));

}

【讨论】:

    猜你喜欢
    • 2014-05-15
    • 1970-01-01
    • 2020-01-03
    • 2020-09-22
    • 1970-01-01
    • 2018-06-18
    • 2011-06-20
    • 2012-01-22
    • 2011-09-15
    相关资源
    最近更新 更多