Частный метод модульного тестирования в классе управления ресурсами (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 ответов:
В принципе, это звучит так, как будто вы хотите макет, чтобы сделать модульное тестирование более осуществимым. Способ сделать класс таргетируемым для модульного тестирования независимо от иерархии объектов и внешних зависимостей-это инъекция зависимостей . Создайте класс "FooFileReader" следующим образом:
class FooFileReader { public: virtual std::ostream& GetFileStream() = 0; };
Сделайте две реализации, одна из которых открывает файл и предоставляет его в виде потока (или массива байтов, если это то, что вам действительно нужно.) Другой-это макет объекта, который просто возвращает тестовые данные, предназначенные для того, чтобы подчеркнуть ваш алгоритм.
Теперь сделайте конструктор foo такой сигнатурой:
Теперь вы можете построить foo для модульного тестирования, передав макет объекта, или построить его с реальным файлом, используя реализацию, которая открывает файл. Оберните конструкцию "реального" Foo в фабрику , чтобы упростить клиентам получение правильной реализации.Foo(FooFileReader* pReader)
Используя этот подход, нет никаких причин не тестировать против " 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
, Вы можете построить и протестировать эти два класса в полной изоляции.
Ну, предпочтительным способом в 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
. Хотя на самом деле он не является синглетным и поэтому все еще поддается модульному тестированию, это не элегантное решение. Я думаю, что передача обработчика - это лучший вариант прямо сейчас.