Закрытый метод модульного тестирования в классе управления ресурсами (C ++) - PullRequest
6 голосов
/ 12 февраля 2010

Ранее я задавал этот вопрос под другим именем, но удалил его, потому что плохо объяснил.

Допустим, у меня есть класс, который управляет файлом. Допустим, этот класс обрабатывает файл как имеющий определенный формат файла и содержит методы для выполнения операций с этим файлом:

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.

Кто-нибудь знает решение этой проблемы?

Ответы [ 6 ]

3 голосов
/ 12 февраля 2010

Один из способов состоит в том, чтобы извлечь метод контрольной суммы в его собственный класс и иметь открытый интерфейс для тестирования.

2 голосов
/ 12 февраля 2010

По сути, это звучит так, будто вы хотите, чтобы макет сделал юнит-тестирование более осуществимым. Способ сделать класс нацеливаемым для модульного тестирования независимо от иерархии объектов и внешних зависимостей с помощью внедрения зависимостей . Создайте класс "FooFileReader" примерно так:

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

Создайте две реализации: одна откроет файл и отобразит его в виде потока (или массива байтов, если это то, что вам действительно нужно). Другая представляет собой фиктивный объект, который просто возвращает тестовые данные, разработанные для усиления вашего алгоритма.

Теперь, чтобы у конструктора foo была эта подпись:

Foo(FooFileReader* pReader)

Теперь вы можете создать foo для модульного тестирования, передав фиктивный объект, или создать его с реальным файлом, используя реализацию, открывающую файл. Оберните конструкцию «настоящего» Foo на фабрике , чтобы клиентам было проще получить правильную реализацию.

Используя этот подход, нет причин не проверять int "getChecksum ()", так как его реализация теперь будет использовать фиктивный объект.

1 голос
/ 12 февраля 2010
#ifdef TEST
#define private public
#endif

// access whatever you'd like to test here
1 голос
/ 12 февраля 2010

Я бы начал с извлечения кода вычисления контрольной суммы в свой собственный класс:

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, вы можете создать и протестировать эти два класса в полной изоляции.

1 голос
/ 12 февраля 2010

Простой прямой ответ - сделать класс юнит-теста другом тестируемого класса. Таким образом, класс модульного теста может получить доступ к calculateChecksum(), даже если он является закрытым.

Еще одна возможность взглянуть на то, что у Foo, похоже, есть ряд несвязанных обязанностей, и может возникнуть необходимость в перефакторинге. Вполне возможно, что вычисление контрольной суммы вообще не должно быть частью Foo. Вместо этого вычисление контрольной суммы может быть лучше в качестве алгоритма общего назначения, который любой может применить по мере необходимости (или, возможно, своего рода обратное - функтор для использования с другим алгоритмом, таким как std::accumulate).

0 голосов
/ 12 марта 2010

Что ж, предпочтительный путь в C ++ для файлового ввода-вывода - потоковый. Таким образом, в приведенном выше примере было бы гораздо разумнее добавить поток вместо имени файла. Например,

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

Таким образом, вы можете использовать std::stringstream для модульного тестирования и иметь полный контроль над тестом.

Если вы не хотите использовать потоки, то можно использовать стандартный пример шаблона RAII, определяющего класс File. В таком случае «простой» способ - создать чистый класс виртуального интерфейса 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. Хотя на самом деле это не единичный и, следовательно, тестируемый модуль, это не элегантное решение. Я думаю, что передача обработчика - лучший вариант прямо сейчас.

...