6

我之前用另一个名字问过这个问题,但是因为我没有很好地解释它而将其删除。

假设我有一个管理文件的类。假设此类将文件视为具有特定文件格式,并包含对该文件执行操作的方法:

class Foo {
    std::wstring fileName_;
public:
    Foo(const std::wstring& fileName) : fileName_(fileName)
    {
        //Construct a Foo here.
    };
    int getChecksum()
    {
        //Open the file and read some part of it

        //Long method to figure out what checksum it is.

        //Return the checksum.
    }
};

假设我希望能够对此类计算校验和的部分进行单元测试。对加载到文件中的类的部分进行单元测试是不切实际的,因为要测试getChecksum()方法的每个部分,我可能需要构建 40 或 50 个文件!

现在假设我想在类的其他地方重用校验和方法。我提取该方法,使其现在看起来像这样:

class Foo {
    std::wstring fileName_;
    static int calculateChecksum(const std::vector<unsigned char> &fileBytes)
    {
        //Long method to figure out what checksum it is.
    }
public:
    Foo(const std::wstring& fileName) : fileName_(fileName)
    {
        //Construct a Foo here.
    };
    int getChecksum()
    {
        //Open the file and read some part of it

        return calculateChecksum( something );
    }
    void modifyThisFileSomehow()
    {
        //Perform modification

        int newChecksum = calculateChecksum( something );

        //Apply the newChecksum to the file
    }
};

现在我想对该calculateChecksum()方法进行单元测试,因为它易于测试且复杂,我不关心单元测试getChecksum(),因为它简单且很难测试。但我不能calculateChecksum()直接测试,因为它是private.

有谁知道这个问题的解决方案?

4

6 回答 6

3

一种方法是将校验和方法提取到它自己的类中,并使用一个公共接口进行测试。

于 2010-02-12T18:13:31.373 回答
2

基本上,听起来你想要一个模拟来使单元测试更可行。使一个类可以独立于对象层次结构和外部依赖项进行单元测试的方法是通过依赖项注入。像这样创建一个类“FooFileReader”:

class FooFileReader
{
public:
   virtual std::ostream& GetFileStream() = 0;
};

进行两种实现,一种打开文件并将其公开为流(或字节数组,如果这是您真正需要的)。另一种是模拟对象,仅返回旨在对您的算法施加压力的测试数据。

现在,让 foo 构造函数有这个签名:

Foo(FooFileReader* pReader)

现在,您可以通过传递一个模拟对象来构造 foo 以进行单元测试,或者使用打开文件的实现使用真实文件来构造它。在工厂中包装“真实” Foo 的构造,以使客户更容易获得正确的实现。

通过使用这种方法,没有理由不针对“int getChecksum()”进行测试,因为它的实现现在将使用模拟对象。

于 2010-02-12T18:13:24.680 回答
1

我首先将校验和计算代码提取到它自己的类中:

class CheckSumCalculator {
    std::wstring fileName_;

public:
    CheckSumCalculator(const std::wstring& fileName) : fileName_(fileName)
    {
    };

    int doCalculation()
    {
        // Complex logic to calculate a checksum
    }
};

这使得单独测试校验和计算变得非常容易。但是,您可以更进一步并创建一个简单的界面:

class FileCalculator {

public:
    virtual int doCalculation() =0;
};

和实施:

class CheckSumCalculator : public FileCalculator {
    std::wstring fileName_;

public:
    CheckSumCalculator(const std::wstring& fileName) : fileName_(fileName)
    {
    };

    virtual int doCalculation()
    {
        // Complex logic to calculate a checksum
    }
};

然后将FileCalculator接口传递给您的Foo构造函数:

class Foo {
    std::wstring fileName_;
    FileCalculator& fileCalc_;
public:
    Foo(const std::wstring& fileName, FileCalculator& fileCalc) : 
        fileName_(fileName), 
        fileCalc_(fileCalc)
    {
        //Construct a Foo here.
    };

    int getChecksum()
    {
        //Open the file and read some part of it

        return fileCalc_.doCalculation( something );
    }

    void modifyThisFileSomehow()
    {
        //Perform modification

        int newChecksum = fileCalc_.doCalculation( something );

        //Apply the newChecksum to the file
    }
};

在您的真实生产代码中,您将创建 aCheckSumCalculator并将其传递给Foo,但在您的单元测试代码中,您可以创建 a Fake_CheckSumCalculator(例如,始终返回已知的预定义校验和)。

现在,即使Foo依赖于CheckSumCalculator,您也可以完全隔离地构造和单元测试这两个类。

于 2010-02-12T18:31:36.977 回答
1

简单直接的答案是让你的单元测试类成为被测类的朋友。这样,单元测试类calculateChecksum()即使是私有的也可以访问。

另一种可能性是 Foo 似乎有许多不相关的职责,并且可能需要重构。很可能,计算校验和根本不应该是其中的一部分Foo。相反,将校验和计算为任何人都可以根据需要应用的通用算法可能会更好(或者可能是相反的一种 - 与其他算法一起使用的函子,如std::accumulate)。

于 2010-02-12T18:11:12.330 回答
1
#ifdef TEST
#define private public
#endif

// access whatever you'd like to test here
于 2010-02-12T18:37:15.480 回答
0

那么在 C++ 中文件 IO 的首选方式是通过流。因此,在上面的示例中,注入流而不是文件名可能更有意义。例如,

Foo(const std::stream& file) : file_(file)

通过这种方式,您可以std::stringstream用于单元测试并完全控制测试。

如果您不想使用流,则File可以使用定义类的 RAII 模式的标准示例。然后进行的“简单”方法是创建一个纯虚拟接口类File,然后创建该接口的实现。然后Foo该类将使用接口类 File。例如,

Foo(const File& file) : file_(file)

然后通过简单地创建一个简单的子类File并将其注入(存根)来完成测试。也可以创建一个模拟类(例如参见 Google Mock)。

但是,您可能还希望对File实现类进行单元测试,并且由于它是 RAII,因此它又需要一些依赖注入。我通常尝试创建一个纯虚拟接口类,它只提供基本的 C 文件操作(打开、关闭、读取、写入等或 fopen、fclose、fwrite、fread 等)。例如,

class FileHandler {
public:
    virtual ~FileHandler() {}
    virtual int open(const char* filename, int flags) = 0;
    // ... and all the rest
};

class FileHandlerImpl : public FileHandlerImpl {
public:
    virtual int open(const char* filename, int flags) {
        return ::open(filename, flags);
    }
    // ... and all the rest in exactly the same maner
};

这个FileHandlerImpl类非常简单,我不对其进行单元测试。但是,好处是在类的构造函数中使用它FileImpl可以轻松地对FileImpl类进行单元测试。例如,

FileImple(const FileHandler& fileHandler, const std::string& fileName) : 
    mFileHandler(fileHandler), mFileName(fileName)

到目前为止唯一的缺点是FileHandler必须传递。我曾想过使用该FileHandle接口来实际提供一个静态实例集/获取方法,可用于获取FileHandler对象的单个全局实例。虽然不是真正的单例,因此仍然是可单元测试的,但它不是一个优雅的解决方案。我想现在传递一个处理程序是最好的选择。

于 2010-03-12T08:55:27.333 回答