Allen Unerschrockenen auf dem Weg von der Verleugnung zur Überzeugung ist gewidmet ...
Entwickler sind sich einig, dass ein Programmierer, der den Code nicht mit Tests abdeckt, einfach nicht versteht, warum sie benötigt werden und wie sie vorbereitet werden sollen. Es ist schwer, dem zu widersprechen, wenn Sie bereits verstehen, worum es geht. Aber wie kann dieses wertvolle Verständnis erreicht werden?
Kein Schicksal...
Es kommt daher vor, dass die offensichtlichsten Dinge unter den Tonnen nützlicher Informationen im globalen Netzwerk oft keine klare Beschreibung haben. Eine Art regulärer Bewerber beschließt, sich mit der dringenden Frage „Was sind Unit-Tests?“ Zu befassen und stößt auf viele solcher Beispiele, die wie ein Transparentpapier von Artikel zu Artikel kopiert werden:
"Wir haben eine Methode, die die Summe der Zahlen berechnet"
public Integer sum (Integer a, Integer b) {
return a + b
}
"Sie können einen Test für diese Methode schreiben"
Prüfung
public void testGoodOne () {
assertThat (Summe (2,2), ist (4));
}}
Dies ist kein Scherz, sondern ein vereinfachtes Beispiel aus einem typischen Artikel über Unit-Testing-Technologie, in dem am Anfang und am Ende allgemeine Sätze über Nutzen und Notwendigkeit stehen und in der Mitte ...
Dies zu sehen und es aus Gründen des Glaubens zweimal zu lesen, ruft der Antragsteller aus: „Was für ein grausamer Unsinn ? .. ”Schließlich gibt es in seinem Code praktisch keine Methoden, die durch Argumente alles bekommen, was sie brauchen, und dann ein eindeutiges Ergebnis für sie liefern. Dies sind typische utilitaristische Methoden, die sich kaum ändern. Aber was ist mit komplexen Prozeduren, injizierten Abhängigkeiten und Methoden, ohne Werte zurückzugeben? Dort ist dieser Ansatz ab dem Wort „absolut“ nicht anwendbar.
Wenn der hartnäckige Bewerber zu diesem Zeitpunkt nicht mit der Hand winkt und weiter taucht, stellt er bald fest, dass MOCs für Abhängigkeiten verwendet werden, für deren Methoden ein bedingtes Verhalten definiert ist, nämlich ein Stub. Hier kann der Antragsteller völlig umhauen, wenn es keine freundliche und geduldige mittlere / ältere Person gibt, die bereit und in der Lage ist, alles zu erklären ... Andernfalls verliert der Antragsteller für die Wahrheit vollständig die Bedeutung von „Was sind Unit-Tests?“, Da sich die meisten der getesteten Methoden als eine Art Schein-Fiktion herausstellen und was in diesem Fall getestet wird, ist nicht klar. Darüber hinaus ist nicht klar, wie dies für eine große, mehrschichtige Anwendung organisiert werden soll und warum dies erforderlich ist. Im besten Fall wird die Frage also auf bessere Zeiten verschoben, im schlimmsten Fall - sie versteckt sich in einer Schachtel mit verdammten Dingen.
Das Ärgerlichste ist, dass die Testabdeckungstechnologie einfach und für jedermann zugänglich ist und ihre Vorteile so offensichtlich sind, dass alle Ausreden für sachkundige Personen naiv erscheinen. Aber um es herauszufinden, fehlt dem Anfänger eine sehr kleine, elementare Essenz, wie das Umlegen eines Schalters.
Schlüsselmission
Zunächst schlage ich vor, die Schlüsselfunktion (Mission) der Komponententests und den Tastengewinn auf den Punkt zu bringen. Hier gibt es verschiedene malerische Optionen, aber ich schlage vor, diese in Betracht zu ziehen: Die
Schlüsselfunktion von Komponententests besteht darin, das erwartete Verhalten des Systems zu erfassen.
und dieses: Der
Hauptvorteil von Komponententests ist die Fähigkeit, alle Funktionen der Anwendung in Sekundenschnelle "auszuführen".
Ich empfehle, sich dies für Interviews zu merken und werde es ein wenig erklären. Jede Funktionalität impliziert Verwendungsregeln und Ergebnisse. Diese Anforderungen kommen aus dem Geschäft durch Systemanalysen und werden in Code implementiert. Der Code wird jedoch ständig weiterentwickelt, es entstehen neue Anforderungen und Verbesserungen, die die fertige Funktionalität unmerklich und unerwartet verändern können. Hier stehen Unit-Tests auf der Hut, die die genehmigten Regeln festlegen, nach denen das System funktionieren soll! Die Tests zeichnen ein Szenario auf, das für das Unternehmen wichtig ist. Wenn der Test nach der nächsten Überarbeitung fehlschlägt, fehlt etwas: Entweder der Entwickler oder der Analyst haben sich geirrt, oder die neuen Anforderungen widersprechen den bestehenden und sollten geklärt werden usw. Das Wichtigste ist, dass die „Überraschung“ nicht durchgerutscht ist.
Ein einfacher Standard-Unit-Test ermöglichte es, unerwartetes und wahrscheinlich unerwünschtes Systemverhalten frühzeitig zu erkennen. In der Zwischenzeit wächst und erweitert sich das System, die Wahrscheinlichkeit, dass Details fehlen, steigt ebenfalls, und nur Unit-Test-Skripte können sich an alles erinnern und unbemerkte Abweichungen rechtzeitig verhindern. Es ist sehr bequem und zuverlässig, und der Hauptkomfort ist die Geschwindigkeit. Die Anwendung muss nicht einmal Hunderte ihrer Felder, Formulare oder Schaltflächen starten und durchwandern. Sie müssen Tests ausführen und in Sekundenschnelle entweder die volle Bereitschaft oder einen Fehler erhalten.
Denken Sie also daran: Erfassen Sie das erwartete Verhalten in Form von Unit-Test-Skripten und führen Sie die Anwendung sofort aus, ohne sie zu starten. Dies ist der absolute Wert, den Unit-Tests erreichen können.
Aber verdammt noch mal, wie?
Kommen wir zum lustigen Teil. Moderne Anwendungen beseitigen aktiv die Monolithizität. Microservices, Module, "Layer" - die Grundprinzipien für die Organisation von Arbeitscode, mit denen Sie Unabhängigkeit, einfache Wiederverwendung, Austausch und Übertragung auf Systeme usw. erreichen können. Layering und Abhängigkeitsinjektion sind Schlüssel in unserem Thema.
Betrachten Sie die Ebenen einer typischen Webanwendung: Controller, Dienste, Repositorys usw. Darüber hinaus werden Dienstprogramme, Fassaden, Modelle und DTO-Ebenen verwendet. Die letzten beiden sollten keine Funktionalität enthalten, d.h. andere Methoden als Accessoren (Getter / Setter), sodass Sie sie nicht mit Tests abdecken müssen. Wir werden den Rest der Schichten als Ziele für die Abdeckung betrachten.
Unabhängig davon, wie lecker dieser Vergleich ist, kann die Anwendung nicht mit einem Puff Cake verglichen werden, da diese Schichten wie Abhängigkeiten ineinander eingebettet sind:
- Der Controller implementiert die Dienste, die er für das Ergebnis aufruft
- Der Dienst injiziert Repositorys (DAO) in sich selbst und kann Dienstprogrammkomponenten injizieren
- Die Fassade ist so konzipiert, dass sie die Arbeit vieler Dienstleistungen bzw. Komponenten kombiniert und implementiert
Die Hauptidee, all diese Dinge in der gesamten Anwendung zu testen: jede Schicht unabhängig von den anderen Schichten abdecken. Ein Hinweis auf Unabhängigkeit und andere antimonolithische Merkmale. Jene. Wenn ein Repository in den getesteten Dienst eingebettet ist, wird dieser „Gast“ im Rahmen des Testens des Dienstes verspottet, im Rahmen des Repository-Tests jedoch persönlich ehrlich getestet. So werden Tests für jedes Element jeder Schicht erstellt, niemand wird vergessen - alles ist im Geschäft.
Blätterteigprinzip
Kommen wir zu den Beispielen, einer einfachen Anwendung in Java Spring Boot. Der Code ist elementar, sodass die Essenz leicht zu verstehen und in ähnlicher Weise auf andere moderne Sprachen / Frameworks anwendbar ist. Die Anwendung hat eine einfache Aufgabe - multiplizieren Sie die Zahl mit 3, d. H. dreifach, aber gleichzeitig werden wir eine mehrschichtige Anwendung mit Abhängigkeitsinjektion und mehrschichtiger Abdeckung von Kopf bis Fuß erstellen.
Die Struktur enthält Pakete für drei Ebenen: Controller, Service, Repo. Die Struktur der Tests ist ähnlich.
Die Anwendung funktioniert folgendermaßen:
- Vom Front-End kommt eine GET-Anfrage an die Steuerung mit der Kennung der Nummer, die verdreifacht werden muss.
- Der Controller fordert das Ergebnis von seiner Dienstabhängigkeit an
- Der Dienst fordert Daten von seinem Abhängigkeits-Repository an, multipliziert sie und gibt das Ergebnis an den Controller zurück
- Der Controller ergänzt das Ergebnis und kehrt zum Frontend zurück
Beginnen wir mit dem Controller:
@RestController
@RequiredArgsConstructor
public class SomeController {
private final SomeService someService; // dependency injection
static final String RESP_PREFIX = ": ";
static final String PATH_GET_TRIPLE = "/triple/{numberId}";
@GetMapping(path = PATH_GET_TRIPLE) // mapping method to GET with url=path
public ResponseEntity<String> triple(@PathVariable(name = "numberId") int numberId) {
int res = someService.tripleMethod(numberId); // dependency call
String resp = RESP_PREFIX + res; // own logic
return ResponseEntity.ok().body(resp);
}
}
Ein typischer Rest-Controller verfügt über eine Abhängigkeitsinjektion someService. Die Triple-Methode ist für eine GET-Anforderung an die URL "/ Triple / {numberId}" konfiguriert, wobei die Nummernkennung in der Pfadvariablen übergeben wird. Die Methode selbst kann in zwei Hauptkomponenten unterteilt werden:
- Zugriff auf eine Abhängigkeit - Anfordern von Daten von außen oder Aufrufen einer Prozedur ohne Ergebnis
- eigene Logik - Arbeiten mit vorhandenen Daten
Betrachten Sie einen Service:
@Service
@RequiredArgsConstructor
public class SomeService {
private final SomeRepository someRepository; // dependency injection
public int tripleMethod(int numberId) {
Integer fromDB = someRepository.findOne(numberId); // dependency call
int res = fromDB * 3; // own logic
return res;
}
}
Hier ist eine ähnliche Situation: Injizieren der someRepository-Abhängigkeit, und die Methode besteht aus dem Zugriff auf die Abhängigkeit und ihre eigene Logik.
Schließlich - das Repository der Einfachheit halber ohne Datenbank:
@Repository
public class SomeRepository {
public Integer findOne(Integer id){
return id;
}
}
Die bedingte Methode findOne durchsucht angeblich die Datenbank nach einem Wert nach Bezeichner, gibt jedoch einfach dieselbe Ganzzahl zurück. Dies hat keinen Einfluss auf das Wesentliche unseres Beispiels.
Wenn Sie unsere Anwendung ausführen, können Sie anhand der konfigurierten URL Folgendes sehen:
Funktioniert! Geschichtet! In der Produktion ...
Oh ja, Tests ...
Ein wenig über die Essenz. Das Schreiben von Tests ist auch ein kreativer Prozess! Daher ist die Ausrede „Ich bin ein Entwickler, kein Tester“ völlig unangemessen. Ein guter Test erfordert ebenso wie eine gute Funktionalität Einfallsreichtum und Schönheit. Zunächst muss jedoch die Grundstruktur des Tests festgelegt werden.
Die Testklasse enthält Methoden, die die Methoden der Zielklasse testen. Das Minimum, das jede Testmethode enthalten sollte, ist ein Aufruf der entsprechenden Methode der Zielklasse, bedingt so:
@Test
void someMethod_test() {
// prepare...
int res = someService.someMethod();
// check...
}
Diese Herausforderung kann von Vorbereitung und Überprüfung umgeben sein. Vorbereiten von Daten, einschließlich Eingabeargumenten und Beschreiben des Verhaltens der Mocks. Die Validierung der Ergebnisse ist normalerweise ein Vergleich mit dem erwarteten Wert. Erinnern Sie sich daran, das erwartete Verhalten erfasst zu haben? Insgesamt ist ein Test ein Szenario, das eine Situation simuliert und aufzeichnet, dass sie wie erwartet bestanden wurde und die erwarteten Ergebnisse zurückgibt.
Versuchen wir am Beispiel des Controllers, den grundlegenden Testschreibalgorithmus detailliert darzustellen. Zunächst verwendet die Zielcontrollermethode einen int numberId-Parameter. Fügen wir ihn unserem Skript hinzu:
int numberId = 42; // input path variable
Dieselbe numberId wird während der Übertragung an die Eingabe für die Servicemethode übertragen, und jetzt ist es an der Zeit, den Service-Mock bereitzustellen:
@MockBean
private SomeService someService;
Der Methodencode des Controllers arbeitet mit dem vom Service empfangenen Ergebnis. Wir simulieren dieses Ergebnis sowie einen Aufruf, der es zurückgibt:
int serviceRes = numberId*3; // result from mock someService
// prepare someService.tripleMethod behavior
when(someService.tripleMethod(eq(numberId))).thenReturn(serviceRes);
Dieser Eintrag bedeutet: "Wenn someService.tripleMethod mit einem Argument gleich numberId aufgerufen wird, wird der Wert von serviceRes zurückgegeben."
Darüber hinaus erfasst dieser Datensatz die Tatsache, dass diese Dienstmethode aufgerufen werden sollte, was ein wichtiger Punkt ist. Es kommt vor, dass Sie einen Aufruf einer Prozedur ohne Ergebnis reparieren müssen, dann wird eine andere Notation verwendet, üblicherweise wie folgt: "Nichts tun, wenn ...":
Mockito.doNothing().when(someService).someMethod(eq(someParam));
Auch hier ist nur eine Nachahmung der Arbeit von someService. Ehrliche Tests mit detaillierter Korrektur des Verhaltens von someService werden separat implementiert. Außerdem spielt es hier keine Rolle, dass sich der Wert verdreifacht, wenn wir schreiben
int serviceRes = numberId*5;
Dadurch wird das aktuelle Skript nicht beschädigt, da Hier wird nicht das Verhalten von someService erfasst, sondern das Verhalten des Controllers, das das Ergebnis von someService als selbstverständlich ansieht. Dies ist völlig logisch, da die Zielklasse nicht für das Verhalten der injizierten Abhängigkeit verantwortlich sein kann, sondern ihr vertrauen muss.
Daher haben wir das Verhalten des Mocks in unserem Skript definiert. Wenn der Test ausgeführt wird und der Aufruf der Zielmethode zum Verspotten erfolgt, gibt er das zurück, was angefordert wurde - serviceRes, und dann arbeitet der eigene Code des Controllers mit diesem Wert.
Als Nächstes rufen wir die Zielmethode im Skript auf. Die Controller-Methode hat eine Besonderheit: Sie wird im Code nicht explizit aufgerufen, sondern über die HTTP-GET-Methode und die URL gebunden. In Tests wird sie daher über einen speziellen Testclient aufgerufen. Im Frühjahr ist dies MockMvc. In anderen Frameworks gibt es Analoga, z. B. WebTestCase.createClient in Symfony. Daher ist es einfach, die Controller-Methode durch Zuordnung nach GET und URL auszuführen.
//// mockMvc.perform
MockHttpServletRequestBuilder requestConfig = MockMvcRequestBuilders.get(SomeController.PATH_GET_TRIPLE, numberId);
MvcResult mvcResult = mockMvc.perform(requestConfig)
.andExpect(status().isOk())
//.andDo(MockMvcResultHandlers.print())
.andReturn()
;//// mockMvc.perform
Dort wird auch geprüft, ob eine solche Zuordnung überhaupt existiert. Wenn der Anruf erfolgreich ist, müssen die Ergebnisse überprüft und korrigiert werden. Sie können beispielsweise festlegen, wie oft die Scheinmethode aufgerufen wurde:
// check of calling
Mockito.verify(someService, Mockito.atLeastOnce()).tripleMethod(eq(numberId));
In unserem Fall ist dies überflüssig, da Wir haben bereits den einzigen Aufruf über wann behoben, aber manchmal ist diese Methode angemessen.
Und jetzt die Hauptsache - wir überprüfen das Verhalten des eigenen Codes des Controllers:
// check of result
assertEquals(SomeController.RESP_PREFIX+serviceRes, mvcResult.getResponse().getContentAsString());
Hier haben wir festgelegt, wofür die Methode selbst verantwortlich ist - dass das von someService empfangene Ergebnis mit dem Controller-Präfix verkettet wird und diese Zeile in den Antworttext eingeht. Übrigens können Sie den Inhalt des Körpers mit eigenen Augen sehen, wenn Sie die Linie auskommentieren
//.andDo(MockMvcResultHandlers.print())
Normalerweise wird dieser Druck auf der Konsole jedoch nur als Hilfe beim Debuggen verwendet.
Somit haben wir eine Testmethode in der Controller-Testklasse:
@WebMvcTest(SomeController.class)
class SomeControllerTest {
@MockBean
private SomeService someService;
@Autowired
private MockMvc mockMvc;
@Test
void triple() throws Exception {
int numberId = 42; // input path variable
int serviceRes = numberId*3; // result from mock someService
// prepare someService.tripleMethod behavior
when(someService.tripleMethod(eq(numberId))).thenReturn(serviceRes);
//// mockMvc.perform
MockHttpServletRequestBuilder requestConfig = MockMvcRequestBuilders.get(SomeController.PATH_GET_TRIPLE, numberId);
MvcResult mvcResult = mockMvc.perform(requestConfig)
.andExpect(status().isOk())
//.andDo(MockMvcResultHandlers.print())
.andReturn()
;//// mockMvc.perform
// check of calling
Mockito.verify(someService, Mockito.atLeastOnce()).tripleMethod(eq(numberId));
// check of result
assertEquals(SomeController.RESP_PREFIX+serviceRes, mvcResult.getResponse().getContentAsString());
}
}
Jetzt ist es an der Zeit, die someService.tripleMethod-Methode ehrlich zu testen, bei der es ebenfalls einen Abhängigkeitsaufruf und Ihren eigenen Code gibt. Bereiten Sie ein beliebiges Eingabeargument vor und simulieren Sie das Verhalten der someRepository-Abhängigkeit:
int numberId = 42;
when(someRepository.findOne(eq(numberId))).then(AdditionalAnswers.returnsFirstArg());
Übersetzung: "Wenn someRepository.findOne mit einem Argument gleich numberId aufgerufen wird, wird dasselbe Argument zurückgegeben." Eine ähnliche Situation - hier überprüfen wir nicht die Logik der Abhängigkeit, sondern nehmen ihr Wort dafür. Wir erfassen nur den Aufruf der Abhängigkeit innerhalb dieser Methode. Das Prinzip hier ist die eigene Logik des Dienstes, sein Verantwortungsbereich:
assertEquals(numberId*3, res);
Wir legen fest, dass der vom Repository empfangene Wert durch die eigene Logik der Methode verdreifacht werden sollte. Jetzt schützt dieser Test diese Anforderung:
@ExtendWith(MockitoExtension.class)
class SomeServiceTest {
@Mock
private SomeRepository someRepository; // ,
@InjectMocks
private SomeService someService; // ,
@Test
void tripleMethod() {
int numberId = 42;
when(someRepository.findOne(eq(numberId))).then(AdditionalAnswers.returnsFirstArg());
int res = someService.tripleMethod(numberId);
assertEquals(numberId*3, res);
}
}
Da unser Repository bedingt Spielzeug ist, hat sich der Test als angemessen erwiesen:
class SomeRepositoryTest {
// no dependency injection
private final SomeRepository someRepository = new SomeRepository();
@Test
void findOne() {
int id = 777;
Integer fromDB = someRepository.findOne(id);
assertEquals(id, fromDB);
}
}
Aber auch hier ist das gesamte Skelett vorhanden: Vorbereitung, Anrufung und Überprüfung. Somit ist die korrekte Arbeit von someRepository.findOne behoben.
Ein echtes Repository erfordert Tests mit dem Erhöhen der Datenbank im Speicher oder in einem Testcontainer, dem Migrieren der Struktur und der Daten und manchmal dem Einfügen von Testdatensätzen. Dies ist oft die längste Testschicht, aber nicht weniger wichtig, weil Erfolgreiche Migration, Speichern von Modellen, korrekte Auswahl usw. werden aufgezeichnet. Die Organisation von Datenbanktests geht über den Rahmen dieses Artikels hinaus, wird jedoch in den Handbüchern ausführlich beschrieben. Es gibt keine Abhängigkeitsinjektion im Repository und ist nicht erforderlich. Die Aufgabe besteht darin, mit der Datenbank zu arbeiten. In unserem Fall wäre dies ein Test mit vorläufiger Speicherung eines Datensatzes in der Datenbank und anschließender Suche nach ID.
Damit haben wir eine vollständige Abdeckung der gesamten Funktionskette erreicht. Jeder Test ist für die Ausführung seines eigenen Codes verantwortlich und erfasst Aufrufe an alle Abhängigkeiten. Das Testen einer Anwendung erfordert nicht das Ausführen mit vollständiger Kontexterhöhung, was umständlich und zeitaufwändig ist. Die Aufrechterhaltung der Funktionalität durch schnelle und einfache Komponententests schafft eine komfortable und zuverlässige Arbeitsumgebung.
Darüber hinaus verbessern Tests die Qualität des Codes. Im Rahmen unabhängiger Tests in Ebenen müssen Sie häufig die Organisation Ihres Codes überdenken. Zum Beispiel wurde zuerst eine Methode im Service erstellt, sie ist nicht klein, sie enthält sowohl eigenen Code als auch Mocks, und zum Beispiel ist es nicht sinnvoll, sie zu teilen, sie wird vollständig von den Tests abgedeckt - alle Vorbereitungen und Prüfungen werden definiert. Dann beschließt jemand, dem Dienst eine zweite Methode hinzuzufügen, die die erste Methode aufruft. Es scheint einmal eine häufige Situation zu sein, aber wenn es um die Abdeckung mit einem Test geht, passt etwas nicht zusammen ... Müssen Sie für die zweite Methode das zweite Szenario beschreiben und das erste Vorbereitungsszenario duplizieren? Schließlich funktioniert es nicht, die erste Methode der getesteten Klasse selbst zu sperren.
Vielleicht ist es in diesem Fall angebracht, über eine andere Organisation des Codes nachzudenken. Es gibt zwei entgegengesetzte Ansätze:
- Verschieben Sie die erste Methode in eine Dienstprogrammkomponente, die als Abhängigkeit in den Dienst eingefügt wird.
- Verschieben Sie die zweite Methode in eine Service-Fassade, die verschiedene Methoden des eingebetteten Service oder sogar mehrere Services kombiniert.
Beide Optionen passen gut in das "Ebenen" -Prinzip und werden bequem mit Abhängigkeitsverspottung getestet. Das Schöne ist, dass jede Schicht für ihre eigene Arbeit verantwortlich ist und zusammen einen soliden Rahmen für die Unverwundbarkeit des gesamten Systems schafft.
Auf der Strecke ...
Interviewfrage: Wie oft sollte ein Entwickler Tests innerhalb eines Tickets durchführen? So viele wie Sie möchten, aber mindestens zweimal:
- Stellen Sie vor Arbeitsbeginn sicher, dass alles in Ordnung ist, und stellen Sie später nicht fest, was bereits kaputt ist, und nicht Sie
- am Ende der Arbeit
Warum also Tests schreiben? Damit es sich nicht lohnt, sich alles in einer großen, komplexen Anwendung zu merken und vorauszusehen, muss es der Automatisierung anvertraut werden. Ein Entwickler, der keine automatischen Tests besitzt, ist nicht bereit, an einem großen Projekt teilzunehmen. Jeder Befragte wird dies sofort offenlegen.
Daher empfehle ich, diese Fähigkeiten zu entwickeln, wenn Sie sich für hohe Gehälter qualifizieren möchten. Sie können diese aufregende Übung mit grundlegenden Dingen beginnen, nämlich im Rahmen Ihres bevorzugten Rahmens lernen, wie man testet:
- Komponenten mit eingebetteten Abhängigkeiten, Verspottungstechniken
- Controller, weil Es gibt Nuancen, den Endpunkt zu nennen
- DAO, Repositorys, einschließlich Erhöhung der Testbasis und Migrationen
Ich hoffe, dass dieses Konzept des "Blätterteigs" dazu beigetragen hat, die Technik des Testens komplexer Anwendungen zu verstehen und zu spüren, wie flexibel und leistungsfähig uns das Werkzeug für die Arbeit präsentiert wird. Je besser das Werkzeug ist, desto geschickter ist natürlich die Arbeit, die es erfordert.
Viel Spaß bei Ihrer Arbeit und viel Können!
Der Beispielcode ist über den Link auf github.com verfügbar: https://github.com/denisorlov/examples/tree/main/unittestidea