Частный метод модульного тестирования в классе управления ресурсами (C++)


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

Предположим, у меня есть класс, который управляет файлом. Предположим, что этот класс обрабатывает файл как имеющий определенный формат файла и содержит методы для выполнения операций с этим файлом:
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 6

6 ответов:

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

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

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

Теперь сделайте конструктор foo такой сигнатурой:

Foo(FooFileReader* pReader)
Теперь вы можете построить foo для модульного тестирования, передав макет объекта, или построить его с реальным файлом, используя реализацию, которая открывает файл. Оберните конструкцию "реального" Foo в фабрику , чтобы упростить клиентам получение правильной реализации.

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

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

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

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

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

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

// access whatever you'd like to test here

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

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

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

Если вы не хотите использовать потоки, то можно использовать стандартный пример шаблона RAII, определяющего класс File. Тогда "простой" способ - создать чистый класс виртуального интерфейса File и затем-реализация интерфейса. Затем класс Foo будет использовать файл класса интерфейса. Например,

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

Тестирование затем выполняется путем простого создания простого подкласса к File и введения его вместо этого (stubbing). Можно также создать макет класса (например, 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. Хотя на самом деле он не является синглетным и поэтому все еще поддается модульному тестированию, это не элегантное решение. Я думаю, что передача обработчика - это лучший вариант прямо сейчас.