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