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

1つの方法は、チェックサムメソッドを独自のクラスに抽出し、テストするためのパブリックインターフェイスを用意することです。

于 2010-02-12T18:13:31.373 に答える
2

基本的に、ユニットテストをより実行可能にするためにモックが必要なようです。オブジェクト階層や外部依存性に関係なく単体テストの対象となるクラスを作成する方法は、依存性注入を使用することです。次のようにクラス「FooFileReader」を作成します。

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

2つの実装を作成します。1つはファイルを開き、それをストリーム(または、本当に必要な場合はバイトの配列)として公開します。もう1つは、アルゴリズムにストレスを与えるように設計されたテストデータを返すだけのモックオブジェクトです。

ここで、fooコンストラクターに次の署名を持たせます。

Foo(FooFileReader* pReader)

これで、モックオブジェクトを渡すことで単体テスト用のfooを構築したり、ファイルを開く実装を使用して実際のファイルでfooを構築したりできます。「実際の」Fooの構築を工場でラップして、クライアントが正しい実装を簡単に取得できるようにします。

このアプローチを使用すると、実装でモックオブジェクトが使用されるようになるため、「intgetChecksum()」に対してテストしない理由はありません。

于 2010-02-12T18:13:24.680 に答える
1
#ifdef TEST
#define private public
#endif

// access whatever you'd like to test here
于 2010-02-12T18:37:15.480 に答える
1

簡単で直接的な答えは、ユニットテストクラスをテスト対象クラスの友達にすることです。このようにして、単体テストクラスはcalculateChecksum()プライベートであってもアクセスできます。

注目すべきもう1つの可能性は、Fooが多くの無関係な責任を負っているように見え、リファクタリングが原因である可能性があることです。おそらく、チェックサムの計算は実際にはまったく含まれるべきではありませんFoo。代わりに、チェックサムを計算する方が、必要に応じて誰でも適用できる汎用アルゴリズムとして適している場合があります(または、逆の場合、のような別のアルゴリズムで使用するファンクターstd::accumulate)。

于 2010-02-12T18:11:12.330 に答える
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
    }
};

実際の本番コードでは、を作成してCheckSumCalculatorに渡しFooますが、単体テストコードでは、を作成できますFake_CheckSumCalculator(たとえば、常に既知の事前定義されたチェックサムを返します)。

これで、Fooに依存しCheckSumCalculatorますが、これら2つのクラスを完全に分離して構築および単体テストできます。

于 2010-02-12T18:31:36.977 に答える
0

ファイルIOのC++での推奨される方法は、ストリームによるものです。したがって、上記の例では、ファイル名の代わりにストリームを挿入する方がはるかに理にかなっています。例えば、

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

このようにして、単体テストに使用std::stringstreamし、テストを完全に制御できます。

ストリームを使用したくない場合は、Fileクラスを定義するRAIIパターンの標準的な例を使用できます。次に進む「簡単な」方法は、純粋な仮想インターフェイスクラスを作成してからFile、インターフェイスの実装を作成することです。そのFoo場合、クラスはインターフェイスクラスFileを使用します。例えば、

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

次に、単純なサブクラスを作成し、File代わりにそれを注入する(スタブする)ことで、テストが実行されます。モッククラスの作成(たとえば、Googleモックを参照)も実行できます。

ただし、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-methodsを実際に提供することを考えました。FileHandler実際にはシングルトンではないため、ユニットでテスト可能ですが、エレガントなソリューションではありません。ハンドラーを渡すのが今のところ最良のオプションだと思います。

于 2010-03-12T08:55:27.333 に答える