Einzelne oder mehrere Dateien zum Unit-Testen einer einzelnen Klasse?

20

Bei der Untersuchung der Best Practices für Unit-Tests zur Erstellung von Richtlinien für mein Unternehmen bin ich auf die Frage gestoßen, ob es besser oder nützlicher ist, Testgeräte (Testklassen) zu trennen oder alle Tests für eine einzelne Klasse in einer Datei zu speichern.

Fwiw, ich beziehe mich auf "Komponententests" in dem reinen Sinne, dass es sich um White-Box-Tests handelt, die auf eine einzelne Klasse abzielen, eine Aussage pro Test, alle Abhängigkeiten werden verspottet usw.

Ein Beispielszenario ist eine Klasse ("Dokument") mit zwei Methoden: CheckIn und CheckOut. Jede Methode implementiert verschiedene Regeln usw., die ihr Verhalten steuern. Nach der Regel "One-Assertion-per-Test" werden für jede Methode mehrere Tests durchgeführt. Ich kann entweder alle Tests in eine einzelne DocumentTestsKlasse mit Namen wie CheckInShouldThrowExceptionWhenUserIsUnauthorizedund einfügen CheckOutShouldThrowExceptionWhenUserIsUnauthorized.

Oder ich könnte zwei separate Testklassen haben: CheckInShouldund CheckOutShould. In diesem Fall würden meine Testnamen gekürzt, aber sie würden so organisiert, dass alle Tests für ein bestimmtes Verhalten (Methode) zusammen sind.

Ich bin mir sicher, dass es Vor- und Nachteile für beide Ansätze gibt, und frage mich, ob jemand mit mehreren Dateien den Weg gegangen ist, und wenn ja, warum? Oder, wenn Sie sich für den Single-File-Ansatz entschieden haben, warum ist er Ihrer Meinung nach besser?

SonOfPirate
quelle
2
Die Namen Ihrer Testmethoden sind zu lang. Vereinfachen Sie sie, auch wenn dies bedeutet, dass eine Testmethode mehrere Aussagen enthält.
Bernard
12
@Bernard: Es wird als schlechte Praxis angesehen, mehrere Aussagen pro Methode zu machen. Lange Namen von Testmethoden werden jedoch NICHT als schlechte Praxis angesehen. Normalerweise dokumentieren wir, was die Methode testet, im Namen selbst. ZB constructor_nullSomeList (), setUserName_UserNameIsNotValid () usw.
c_maker
4
@Bernard: Ich benutze auch lange Testnamen (und nur dort!). Ich liebe sie, weil klar ist, was nicht funktioniert hat (wenn deine Namen eine gute Wahl sind;)).
Sebastian Bauer
Mehrere Behauptungen sind keine schlechte Praxis, wenn sie alle das gleiche Ergebnis testen. Z.B. Sie würden nicht haben testResponseContainsSuccessTrue(), testResponseContainsMyData()und testResponseStatusCodeIsOk(). Sie würden sie in einem einzigen haben testResponse()die drei behauptet hat: assertEquals(200, response.status), assertEquals({"data": "mydata"}, response.data)undassertEquals(true, response.success)
Juha Untinen

Antworten:

18

Es ist selten, aber manchmal ist es sinnvoll, mehrere Testklassen für eine bestimmte zu testende Klasse zu haben. Normalerweise würde ich dies tun, wenn verschiedene Setups erforderlich sind und für eine Teilmenge der Tests gemeinsam genutzt werden.

Carl Manaster
quelle
3
Gutes Beispiel, warum mir dieser Ansatz überhaupt vorgestellt wurde. Wenn ich zum Beispiel die CheckIn-Methode testen möchte, möchte ich immer, dass das zu testende Objekt auf eine Weise eingerichtet wird, aber wenn ich die CheckOut-Methode teste, benötige ich eine andere Einrichtung. Guter Punkt.
SonOfPirate
14

Ich kann keinen überzeugenden Grund dafür erkennen, warum Sie einen Test für eine einzelne Klasse in mehrere Testklassen aufteilen. Da die treibende Idee darin bestehen sollte, den Zusammenhalt auf Klassenebene aufrechtzuerhalten, sollten Sie dies auch auf Testebene anstreben. Nur ein paar zufällige Gründe:

  1. Sie müssen den Setup-Code nicht duplizieren (und mehrere Versionen davon verwalten)
  2. Einfacheres Ausführen aller Tests für eine Klasse aus einer IDE, wenn sie zu einer Testklasse gehören
  3. Einfachere Fehlerbehebung durch Eins-zu-Eins-Zuordnung zwischen Testklassen und Klassen.
Brei
quelle
Gute Argumente. Wenn die getestete Klasse ein furchtbarer Fehler ist, würde ich mehrere Testklassen nicht ausschließen, von denen einige Hilfslogik enthalten. Ich mag keine sehr strengen Regeln. Ansonsten tolle Punkte.
Job
1
Ich würde doppelten Setup-Code beseitigen, indem ich eine gemeinsame Basisklasse für die Test-Fixtures habe, die gegen eine einzelne Klasse arbeiten. Ich bin mir nicht sicher, ob ich den beiden anderen Punkten überhaupt zustimme.
SonOfPirate
In JUnit möchten Sie beispielsweise Dinge mit verschiedenen Läufern testen, was dazu führt, dass unterschiedliche Klassen erforderlich sind.
Joachim Nilsson
Da ich eine Klasse mit einer großen Anzahl von Methoden mit jeweils komplexer Mathematik schreibe, habe ich 85 Tests und nur für 24% der Methoden Tests geschrieben. Ich befinde mich hier, weil ich mich gefragt habe, ob es verrückt ist, diese enorme Anzahl von Tests in separate Kategorien zu unterteilen, damit ich Teilmengen ausführen kann.
Mooing Duck
7

Wenn Sie gezwungen sind, Komponententests für eine Klasse auf mehrere Dateien aufzuteilen, kann dies ein Hinweis darauf sein, dass die Klasse selbst schlecht konzipiert ist. Ich kann mir kein Szenario vorstellen, in dem es vorteilhafter wäre, die Komponententests für eine Klasse aufzuteilen, die dem Prinzip der Einzelverantwortung und anderen bewährten Programmiermethoden angemessen entspricht.

Darüber hinaus ist es akzeptabel, längere Methodennamen in Komponententests zu haben. Wenn Sie dies jedoch stören, können Sie die Namenskonvention für Komponententests jederzeit überdenken, um die Namen zu verkürzen.

FischkorbGordo
quelle
Leider halten sich in der realen Welt nicht alle Klassen an SRP et al. Dies wird zu einem Problem, wenn Sie älteren Code testen.
Péter Török,
3
Ich bin mir nicht sicher, wie SRP sich hier verhält. Ich habe mehrere Methoden in meiner Klasse und muss Komponententests für alle unterschiedlichen Anforderungen schreiben, die ihr Verhalten bestimmen. Ist es besser, eine Testklasse mit 50 Testmethoden oder 5 Testklassen mit jeweils 10 Tests zu haben, die sich auf eine bestimmte Methode in der Testklasse beziehen?
SonOfPirate
2
@SonOfPirate: Wenn Sie so viele Tests benötigen, bedeutet dies möglicherweise, dass Ihre Methoden zu viel tun. SRP ist hier also sehr wichtig. Wenn Sie SRP sowohl auf Klassen als auch auf Methoden anwenden, verfügen Sie über viele kleine Klassen und Methoden, die einfach zu testen sind und nicht so viele Testmethoden benötigen. (Aber ich stimme Péter Török zu, dass beim Testen von Legacy-Code bewährte Praktiken auf dem
Markt
Das beste Beispiel, das ich geben kann, ist die CheckIn-Methode, bei der wir Geschäftsregeln haben, die bestimmen, wer die Aktion ausführen darf (Autorisierung), ob sich das Objekt im richtigen Zustand zum Einchecken befindet, z. B. wenn es ausgecheckt ist und wenn Änderungen vorgenommen werden sind gemacht worden. Es werden Komponententests durchgeführt, die sicherstellen, dass die Autorisierungsregeln durchgesetzt werden, sowie Tests, um sicherzustellen, dass sich die Methode korrekt verhält, wenn CheckIn aufgerufen wird, wenn das Objekt nicht ausgecheckt, nicht geändert usw. Aus diesem Grund gibt es am Ende viele Tests. Schlagen Sie vor, dass dies falsch ist?
SonOfPirate
Ich glaube nicht, dass es schnelle, richtige oder falsche Antworten zu diesem Thema gibt. Wenn das, was Sie tun, für Sie arbeitet, ist das großartig. Dies ist nur meine Perspektive auf eine allgemeine Richtlinie.
FishBasketGordo
2

Eines meiner Argumente gegen die Aufteilung der Tests in mehrere Klassen ist, dass es für andere Entwickler im Team (insbesondere diejenigen, die nicht so versiert sind) schwieriger wird, die vorhandenen Tests zu finden ( Gee, ich frage mich, ob es dafür bereits einen Test gibt) method? Ich frage mich, wo es sein würde? ) und auch, wo neue Tests abgelegt werden sollen ( ich schreibe einen Test für diese Methode in diese Klasse, bin mir aber nicht ganz sicher, ob ich sie in eine neue Datei oder in eine vorhandene Datei einfügen soll eine? )

Für den Fall, dass verschiedene Tests drastisch unterschiedliche Setups erfordern, habe ich gesehen, dass einige TDD-Anwender das "Fixture" oder das "Setup" in verschiedene Klassen / Dateien eingeteilt haben, aber nicht die Tests selbst.

rally25rs
quelle
1

Ich wünschte, ich könnte mich an den Link erinnern, in dem die von mir gewählte Technik zum ersten Mal demonstriert wurde. Im Wesentlichen erstelle ich für jede getestete Klasse eine einzelne abstrakte Klasse, die verschachtelte Test-Fixtures (Klassen) für jedes getestete Mitglied enthält. Dadurch wird die ursprünglich gewünschte Trennung erzielt, aber alle Tests werden für jeden Speicherort in derselben Datei gespeichert. Darüber hinaus wird ein Klassenname generiert, der eine einfache Gruppierung und Sortierung im Testrunner ermöglicht.

Hier ist ein Beispiel, wie dieser Ansatz auf mein ursprüngliches Szenario angewendet wird:

public abstract class DocumentTests : TestBase
{
    [TestClass()]
    public sealed class CheckInShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }

    [TestClass()]
    public sealed class CheckOutShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }
}

Dies führt dazu, dass die folgenden Tests in der Testliste angezeigt werden:

DocumentTests+CheckInShould.ThrowExceptionWhenUserIsNotAuthorized
DocumentTests+CheckOutShould.ThrowExceptionWhenUserIsNotAuthorized

Wenn weitere Tests hinzugefügt werden, können diese einfach gruppiert und nach Klassennamen sortiert werden. Dadurch bleiben auch alle Tests für die zu testende Klasse zusammen aufgelistet.

Ein weiterer Vorteil dieses Ansatzes, den ich zu nutzen gelernt habe, ist die objektorientierte Struktur, die es mir ermöglicht, Code innerhalb der Start- und Bereinigungsmethoden der DocumentTests-Basisklasse zu definieren, der von allen verschachtelten Klassen sowie von jeder Klasse gemeinsam genutzt wird verschachtelte Klasse für die darin enthaltenen Tests.

SonOfPirate
quelle
1

Versuchen Sie für eine Minute anders herum zu denken.

Warum sollten Sie nur eine Testklasse pro Klasse haben? Machst du einen Klassentest oder einen Komponententest? Bist du überhaupt auf Klassen angewiesen ?

Ein Unit-Test soll ein bestimmtes Verhalten in einem bestimmten Kontext testen. Die Tatsache, dass Ihre Klasse bereits ein CheckInMittel hat, sollte es ein Verhalten geben, das es an erster Stelle benötigt.

Wie würden Sie über diesen Pseudocode denken:

// check_in_test.file

class CheckInTest extends TestCase {
        /** @test */
        public function unauthorized_users_cannot_check_in() {
                $this->expectException();

                $document = new Document($unauthorizedUser);

                $document->checkIn();
        }
}

Jetzt testen Sie die checkInMethode nicht direkt . Sie testen stattdessen ein Verhalten (das Sie zum Glück einchecken müssen;)). Wenn Sie es umgestalten Documentund in verschiedene Klassen aufteilen oder eine andere Klasse darin zusammenführen müssen, sind Ihre Testdateien immer noch kohärent. Sie sind immer noch sinnvoll, da Sie ändern beim Refactoring nie die Logik, sondern nur die Struktur.

Eine Testdatei für eine Klasse macht es einfach schwieriger, sie bei Bedarf umzugestalten, und Tests sind auch in Bezug auf die Domäne / Logik des Codes selbst weniger sinnvoll.

Steve Chamaillard
quelle
0

Im Allgemeinen würde ich empfehlen, Tests als Test des Verhaltens der Klasse und nicht als Methode zu betrachten. Das heißt, einige Ihrer Tests müssen möglicherweise beide Methoden für die Klasse aufrufen, um das erwartete Verhalten zu testen.

Normalerweise beginne ich mit einer Unit-Test-Klasse pro Produktionsklasse, kann diese Unit-Test-Klasse jedoch aufgrund des von ihnen getesteten Verhaltens in mehrere Testklassen aufteilen. Mit anderen Worten würde ich empfehlen, die Testklasse nicht in CheckInShould und CheckOutShould aufzuteilen, sondern nach dem Verhalten des Prüflings aufzuteilen.

Christian Horsdal
quelle
0

1. Überlegen Sie, was wahrscheinlich schief gehen wird

Während der ersten Entwicklung wissen Sie ziemlich genau, was Sie tun, und beide Lösungen funktionieren wahrscheinlich einwandfrei.

Interessanter wird es, wenn ein Test nach einer Änderung viel später fehlschlägt. Es gibt zwei Möglichkeiten:

  1. Sie haben ernsthaft gebrochen CheckIn(oder CheckOut). Auch in diesem Fall sind sowohl die Einzeldatei- als auch die Doppeldateilösung in Ordnung.
  2. Sie haben beide CheckInund CheckOut(und möglicherweise ihre Tests) auf eine Weise geändert , die für jeden von ihnen Sinn macht, aber nicht für beide zusammen. Sie haben die Kohärenz des Paares gebrochen. In diesem Fall wird es schwieriger, das Problem zu verstehen, wenn die Tests auf zwei Dateien aufgeteilt werden.

2. Überlegen Sie, wofür Tests verwendet werden

Tests dienen zwei Hauptzwecken:

  1. Überprüfen Sie automatisch, ob das Programm noch richtig funktioniert. Ob sich die Tests in einer oder mehreren Dateien befinden, spielt für diesen Zweck keine Rolle.
  2. Helfen Sie einem menschlichen Leser, das Programm zu verstehen. Dies wird viel einfacher, wenn Tests für eng verwandte Funktionen zusammengehalten werden, da das Erkennen ihrer Kohärenz ein schneller Weg zum Verständnis ist.

So?

Beide Perspektiven legen nahe, dass das Zusammenhalten von Tests helfen kann, aber nicht schadet.

Lutz Prechelt
quelle