2010-02-12 6 views
6

Ich stellte diese Frage vorher unter einem anderen Namen aber löschte es, weil ich es nicht sehr gut erklärte.Einheit testet private Methode in der Ressource, die Klasse (C++) verwaltet

Sagen wir, ich habe eine Klasse, die eine Datei verwaltet. Lassen Sie uns sagen, dass diese Klasse die Datei behandelt ein bestimmtes Dateiformat haben, und enthält Methoden Operationen auf diese Datei auszuführen:

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. 
    } 
}; 

Lassen Sie uns sagen, dass ich das Teil dieser Klasse Unit-Test in der Lage sein möchten, dass berechnet die Prüfsumme. Unit testet die Teile der Klasse, die in der Datei geladen werden, und das ist unpraktisch, weil ich jeden Teil der getChecksum() Methode testen muss, muss ich 40 oder 50 Dateien erstellen!

Jetzt sagen wir, ich möchte die Prüfsummenmethode an einer anderen Stelle in der Klasse wiederverwenden. Ich extrahieren Sie die Methode, so dass es jetzt so aussieht:

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 
    } 
}; 

Nun möchte Ich mag die calculateChecksum() Methode Einheit testen, weil es einfach ist, zu testen und zu kompliziert, und ich kümmere mich nicht um Unit-Tests getChecksum(), weil es einfach und sehr schwierig zu testen. Aber ich kann calculateChecksum() nicht direkt testen, weil es private ist.

Kennt jemand eine Lösung für dieses Problem?

Antwort

2

Grundsätzlich klingt es so, als ob Sie eine mock möchten, um das Testen von Einheiten machbarer zu machen. Die Art, wie Sie eine Klasse unabhängig von der Objekthierarchie und von externen Abhängigkeiten für Komponententests einrichten, erfolgt über dependency injection. Erstellen Sie eine Klasse „FooFileReader“ wie so:

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

Machen Sie zwei Implementierungen, eine, die eine Datei öffnet und setzt es als Stream Das andere ist ein (oder ein Array von Bytes, wenn das ist, was Sie wirklich brauchen.) Mock-Objekt, das nur Testdaten zurückgibt, die entworfen wurden, um Ihren Algorithmus zu betonen.

Nun machen den Foo-Konstruktor hat diese Signatur:

Foo(FooFileReader* pReader) 

Jetzt können Sie foo für Unit-Tests konstruieren, indem ein Mock-Objekt übergeben, oder es mit einer echten Datei konstruieren, um die Implementierung verwenden, die die Datei öffnet . Wickeln Sie die Konstruktion des "echten" Foo in eine factory, um es für Kunden einfacher zu machen, die korrekte Implementierung zu erhalten.

Mit diesem Ansatz gibt es keinen Grund, nicht gegen "int getChecksum()" zu testen, da seine Implementierung jetzt das Mock-Objekt verwendet.

+0

+1 Ich nehme an, das könnte für dieses spezielle Szenario funktionieren - aber ich hoffe auf eine allgemeinere Antwort, die für jede Art von Ressource verwalten würde, sei es Registrierungsschlüssel, Datei, Pipe, etc. Sorry ich war nicht ganz klar genug in der Frage :( –

1

Die einfache, direkte Antwort besteht darin, Ihre Unit-Test-Klasse zu einem Freund der getesteten Klasse zu machen. Auf diese Weise kann die Einheitentestklasse auf calculateChecksum() zugreifen, obwohl sie privat ist.

Eine andere Möglichkeit zu betrachten ist, dass Foo scheint, eine Reihe von nicht verwandten Verantwortlichkeiten zu haben, und wegen Re-Factoring fällig sein sollte. Möglicherweise sollte die Berechnung einer Prüfsumme überhaupt nicht Teil von Foo sein. Stattdessen könnte die Berechnung einer Prüfsumme besser als Allzweckalgorithmus sein, den jeder nach Bedarf anwenden kann (oder möglicherweise umgekehrt) - ein Funktor, der mit einem anderen Algorithmus wie std::accumulate verwendet wird.

+0

+1 Ich mag diese Idee ... gibt es eine Möglichkeit, dies mit boost :: test zu tun? –

+0

@BillyONeal: Sie würden wahrscheinlich besser dran sein, das als eine separate Frage zu fragen - aus der Hand, ich bin mir nicht sicher, aber jemand anderes (der diese Kommentare weniger wahrscheinlich sieht) könnte sehr wohl. –

+0

OK :) Will do. Vielen Dank! –

3

Eine Möglichkeit wäre, die Prüfsummenmethode in eine eigene Klasse zu extrahieren und eine öffentliche Schnittstelle zu haben, mit der getestet werden kann.

+0

Wie ist das besser, als die Prüfsummenmethode 'public' an erster Stelle zu machen? –

+2

Natürlich würde man nicht wollen, dass die Prüfsumme in Foo öffentlich ist; es tut es einfach nicht Ich denke, es liegt nicht in der Verantwortung von Foo, die Prüfsumme zu berechnen, und es sollte in die eigene Klasse gezogen werden, damit Sie es auf die gleiche Weise testen würden benutze es von Foo. – NobodyReally

1

I durch Extraktion der Prüfsumme-Berechnungscode in seine eigene Klasse beginnen würde:

class CheckSumCalculator { 
    std::wstring fileName_; 

public: 
    CheckSumCalculator(const std::wstring& fileName) : fileName_(fileName) 
    { 
    }; 

    int doCalculation() 
    { 
     // Complex logic to calculate a checksum 
    } 
}; 

Dies macht es sehr einfach, die Prüfsummenberechnung in Isolation zu testen. Sie könnten jedoch nehmen sie einen Schritt weiter und eine einfache Schnittstelle erstellen:

class FileCalculator { 

public: 
    virtual int doCalculation() =0; 
}; 

Und die Umsetzung:

class CheckSumCalculator : public FileCalculator { 
    std::wstring fileName_; 

public: 
    CheckSumCalculator(const std::wstring& fileName) : fileName_(fileName) 
    { 
    }; 

    virtual int doCalculation() 
    { 
     // Complex logic to calculate a checksum 
    } 
}; 

Und dann die FileCalculator Schnittstelle zu Ihrem Foo Konstruktor übergeben:

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 
    } 
}; 

In Ihrem echten Produktionscode würden Sie eine CheckSumCalculator erstellen und diese an Foo übergeben, aber in Ihrem Komponententest Code könnten Sie eine Fake_CheckSumCalculator (die zum Beispiel immer eine bekannte vordefinierte Prüfsumme zurückgegeben) erstellen.

Nun, obwohl Foo eine Abhängigkeit von CheckSumCalculator hat, können Sie diese beiden Klassen in vollständiger Isolation konstruieren und Unit testen.

+0

Ja, ich nehme an, das funktioniert, aber macht das nicht den ganzen Punkt kaputt, die Prüfsummenberechnung "privat" zu machen? Es scheint, dass es ein sehr umständliches wa ist y, die Funktion öffentlich zu machen. Wie verbessert dies die Kapselung, anstatt sie zu einer öffentlichen Funktion zu machen? –

+1

BillyONeal: 'private' schützt Daten, Operationen ohne Daten sind ohne Fang und brauchen keinen Schutz. –

+0

Wenn ich einen Design-Trade-Off wählen müsste, hätte ich lieber kleinere Klassen, die unabhängig in einem Test-Harness instanziiert und getestet werden können, im Vergleich zu größeren Klassen mit vielen "privaten" Methoden, die schwer zu testen sind. Dies kann bedeuten, dass einige der "inneren Abläufe" Ihrer Bibliothek jetzt der Welt ausgesetzt sind, aber wenn das Ergebnis besser getesteter Code ist, lohnt es sich normalerweise. –

1
#ifdef TEST 
#define private public 
#endif 

// access whatever you'd like to test here 
+0

Wow, +1 für pure Boshaftigkeit! –

+0

Wenn der Testcode und der tatsächliche Code zusammen kompiliert wurden, würde dies funktionieren. Leider befindet sich der Code, den ich testen möchte, in einer statischen Bibliothek und die Komponententests befinden sich in einem .EXE. :( –

+0

Es spielt keine Rolle. Privater/öffentlicher Zugriff wird nur vom Compiler durchgesetzt und existiert nicht zur Laufzeit. Also würde es genauso funktionieren. Verwenden Sie einfach diese Definition vor dem Include der Header, die Sie verwenden, und Sie – rmn

0

Nun ist der bevorzugte Weg in C++ für Datei IO per Stream. Im obigen Beispiel würde es also viel sinnvoller sein, anstelle eines Dateinamens einen Stream zu injizieren. Zum Beispiel

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

Auf diese Weise Sie std::stringstream für Unit-Tests verwenden könnte und haben die volle Kontrolle über den Test.

Wenn Sie keine Streams verwenden möchten, kann das Standardbeispiel eines RAII-Musters verwendet werden, das eine File-Klasse definiert. Die "einfache" Vorgehensweise besteht darin, eine reine virtuelle Schnittstellenklasse File und dann eine Implementierung der Schnittstelle zu erstellen. Die Klasse Foo würde dann die Schnittstellenklasse Datei verwenden. Zum Beispiel

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

Testing wird dann einfach durch die Schaffung eine einfache Unterklasse File und Injektion erfolgen, dass statt (Anstoßen). Das Erstellen einer Scheinklasse (siehe zum Beispiel Google Mock) kann ebenfalls durchgeführt werden.

Allerdings möchten Sie wahrscheinlich Unit-Test die File Implementierungsklasse als auch, und da es RAII ist, benötigt es wiederum einige Abhängigkeits-Injektion. Normalerweise versuche ich eine reine virtuelle Interface-Klasse zu erstellen, die nur die grundlegenden C-Datei-Operationen (Öffnen, Schließen, Lesen, Schreiben, etc. oder fopen, fclose, fwrite, fread usw.) bereitstellt. Zum Beispiel

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 
}; 

Diese FileHandlerImpl Klasse ist so einfach, dass ich nicht Unit-Test es tun. Der Vorteil ist jedoch, dass die Verwendung in dem Konstruktor der FileImpl Klasse I die Einheit FileImpl problemlos testen kann. Zum Beispiel

FileImple(const FileHandler& fileHandler, const std::string& fileName) : 
    mFileHandler(fileHandler), mFileName(fileName) 

Der einzige Nachteil ist so weit, dass die FileHandler herumgereicht werden muss. Ich habe gedacht, die Schnittstelle FileHandle zu verwenden, um tatsächlich eine statische Instanz set/get-Methoden bereitzustellen, die verwendet werden kann, um eine einzelne globale Instanz eines FileHandler Objekts zu erhalten.Obwohl nicht wirklich ein Singleton und damit noch testbar, ist es keine elegante Lösung. Ich denke, es ist momentan die beste Option, einen Hundeführer zu übergeben.

Verwandte Themen