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