资源管理类中的单元测试私有方法 (C++)

发布于 2024-08-20 21:18:27 字数 1483 浏览 13 评论 0原文

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

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

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 个文件!

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

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() 方法进行单元测试,因为它很容易测试但很复杂,而且我不关心单元测试 < code>getChecksum() 因为它很简单并且很难测试。但我无法直接测试 calculateChecksum(),因为它是 private

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

I previously asked this question under another name but deleted it because I didn't explain it very well.

Let's say I have a class which manages a file. Let's say that this class treats the file as having a specific file format, and contains methods to perform operations on this file:

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.
    }
};

Let's say I'd like to be able to unit test the part of this class that calculates the checksum. Unit testing the parts of the class that load in the file and such is impractical, because to test every part of the getChecksum() method I might need to construct 40 or 50 files!

Now lets say I'd like to reuse the checksum method elsewhere in the class. I extract the method so that it now looks like this:

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

Now I'd like to unit test the calculateChecksum() method because it's easy to test and complicated, and I don't care about unit testing getChecksum() because it's simple and very difficult to test. But I can't test calculateChecksum() directly because it is private.

Does anyone know of a solution to this problem?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

迷途知返 2024-08-27 21:18:27

一种方法是将校验和方法提取到其自己的类中,并具有用于测试的公共接口。

One way would be to extract the checksum method out into its own class and have a public interface with which to test.

兲鉂ぱ嘚淚 2024-08-27 21:18:27

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

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

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

现在,使 foo 构造函数具有以下签名:

Foo(FooFileReader* pReader)

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

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

Basically, it sounds like you want a mock to make unit testing more feasible. The way you make a class targetable for unit testing independantly of the object hierarchy and of external dependencies is through dependency injection. Create a class "FooFileReader" like so:

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

Make two implementations, one that opens a file and exposes it as a stream (or an array of bytes if that is what you really need.) The other is a mock object that just returns test data designed to stress your algorithm.

Now, make the foo constructor have this signature:

Foo(FooFileReader* pReader)

Now, you can construct foo for unit testing by passing a mock object, or construct it with a real file using the implementation that opens the file. Wrap construction of the "real" Foo in a factory to make it simpler for clients to get the correct implementation.

By using this approach, there's no reason not to test against " int getChecksum()" since its implementation will now use the mock object.

︶葆Ⅱㄣ 2024-08-27 21:18:27

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

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

The simple, direct answer is to make your unit-test class a friend of the class under test. This way, the unit test class can access calculateChecksum() even though it's private.

Another possibility to look at is that Foo appears to have a number of unrelated responsibilities, and might be due for re-factoring. Quite possibly, computing a check sum shouldn't really be part of Foo at all. Instead, calculating a checksum might be better off as a general purpose algorithm that anybody can apply as needed (or possibly, kind of the reverse -- a functor to use with another algorithm like std::accumulate).

百合的盛世恋 2024-08-27 21:18:27

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

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

在您真实的生产代码中,您 可以会创建一个 CheckSumCalculator 并将其传递给 Foo,但在单元测试代码中,您可以创建一个 Fake_CheckSumCalculator (例如,它总是返回一个已知的预定义校验和)。

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

I'd start by extracting the checksum-calculation code into it's own class:

class CheckSumCalculator {
    std::wstring fileName_;

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

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

This makes it very easy to test the check-sum calculation in isolation. You could however take it one step further and create a simple interface:

class FileCalculator {

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

And the implementation:

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

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

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

And then pass the FileCalculator interface to your Foo constructor:

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

In your real, production code you would create a CheckSumCalculator and pass it to Foo, but in your Unit Test code you could create a Fake_CheckSumCalculator (that, for example always returned a known predefined checksum).

Now, even though Foo has a dependency on CheckSumCalculator, you can construct and Unit Test these two classes in complete isolation.

风向决定发型 2024-08-27 21:18:27
#ifdef TEST
#define private public
#endif

// access whatever you'd like to test here
#ifdef TEST
#define private public
#endif

// access whatever you'd like to test here
饭团 2024-08-27 21:18:27

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 接口来实际提供静态实例 set/get 方法,可用于获取 FileHandler 对象的单个全局实例。虽然不是真正的单例,因此仍然可以进行单元测试,但它不是一个优雅的解决方案。我想传递一个处理程序是目前最好的选择。

Well the preferred way in C++ for file IO is by stream. So in the example above it would make much more sense maybe to inject a stream instead of a file name. For example,

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

In that way you could use std::stringstream for unit testing and have full control of the test.

If you do not want to use streams then the standard example of a RAII pattern defining a File class can be used. The "simple" way to proceed then is to create a pure virtual interface class File and then an implementation of the interface. The Foo class would then use the interface class File. For example,

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

Testing is then done by simply creating a simple subclass to File and injecting that instead (stubbing). Creating a mock class (see Google Mock for example) can also be done.

However, you probable want to unit test the File implementation class as well and since it is RAII, it in turn needs some dependency injection. I usually try to create an pure virtual interface class that just provide the basic C file operations (open, close, read, write, etc. or fopen, fclose, fwrite, fread, etc). For example,

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

This FileHandlerImpl class is so simple that I do not unit test it. However, the benefit is that using it in the constructor of the FileImpl class I can easily unit test the FileImpl class. For example,

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

The only drawback so far is that the FileHandler has to be passed around. I have thought of using the FileHandle interface to actually provide a static instance set/get-methods that can be used to get a single global instance of a FileHandler object. Although not really a singleton and thus still unit testable it is not a elegant solution. I guess passing a handler around is the best option right now.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文