So sparen Sie mit einer testgetriebenen Entwicklung Geld für einen Therapeuten

Hattest du jemals diesen Zustand?

Bild

Ich möchte Ihnen anhand eines bestimmten Beispiels zeigen, wie TDD die Codequalität verbessern kann.
Weil alles, was ich während des Studiums kennengelernt habe, ziemlich theoretisch war.
So kam es, dass ich zwei fast identische Anwendungen schrieb: eine wurde im klassischen Stil geschrieben, da ich TDD damals nicht kannte, und die zweite - nur mit TDD.

Im Folgenden werde ich zeigen, wo die größten Unterschiede waren.

Persönlich war mir das wichtig, denn jedes Mal, wenn jemand einen Fehler in meinem Code fand, bekam ich ein schweres Minus für das Selbstwertgefühl. Ja, ich habe verstanden, dass Fehler normal sind, jeder schreibt sie, aber das Gefühl der Minderwertigkeit ist nicht verschwunden. Während der Weiterentwicklung des Dienstes wurde mir manchmal klar, dass ich selbst einen so geschrieben habe, dass es mich juckte, alles rauszuwerfen und erneut zu schreiben. Und wie es passiert ist, ist unverständlich. Irgendwie war am Anfang alles in Ordnung, aber nach ein paar Features und nach einer Weile kann man Architektur nicht ohne Tränen betrachten. Obwohl es scheint, dass jeder Schritt der Änderung logisch war. Das Gefühl, dass ich das Produkt meiner eigenen Arbeit nicht mochte, floss reibungslos in das Gefühl ein, dass der Programmierer von mir war, entschuldigen Sie, wie eine Kugel aus Scheiße.

Es stellte sich heraus, dass ich nicht der einzige bin und viele meiner Kollegen ähnliche Empfindungen haben. Und dann beschloss ich, entweder normal schreiben zu lernen oder meinen Beruf zu wechseln. Ich habe eine testgetriebene Entwicklung versucht, um etwas an meinem Programmieransatz zu ändern.

Mit Blick auf die Ergebnisse mehrerer Projekte kann ich sagen, dass TDD eine sauberere Architektur bietet, aber die Entwicklung verlangsamt. Und es ist nicht immer geeignet und nicht für jedermann.

Was ist wieder TDD?


Bild


TDD - Entwicklung durch Testen. Wiki-Artikel hier .
Der klassische Ansatz besteht darin, zuerst eine Anwendung zu schreiben und sie dann mit Tests abzudecken.

TDD-Ansatz - zuerst schreiben wir Tests für die Klasse, dann die Implementierung. Wir bewegen uns durch die Abstraktionsebenen - von der höchsten zur angewendeten - und teilen die Anwendung gleichzeitig in Ebenenklassen auf, aus denen wir das gewünschte Verhalten ordnen , ohne von einer bestimmten Implementierung abhängig zu sein.

Und wenn ich das zum ersten Mal lesen würde, würde ich auch nichts verstehen.
Zu viele abstrakte Wörter: Schauen wir uns ein Beispiel an.
Wir werden eine echte Frühlingsanwendung in Java schreiben, wir werden sie in TDD schreiben und ich werde versuchen, meinen Denkprozess während des Entwicklungsprozesses zu zeigen und am Ende Schlussfolgerungen zu ziehen, ob es sinnvoll ist, Zeit mit TDD zu verbringen oder nicht.

Praktische Aufgabe


Nehmen wir an, wir haben so viel Glück, dass wir die ToR von dem haben, was wir entwickeln müssen. Normalerweise kümmern sich Analysten nicht darum, und es sieht ungefähr so ​​aus:

Es ist notwendig, einen Mikroservice zu entwickeln, der die Möglichkeit des Verkaufs von Waren mit anschließender Lieferung an den Kunden zu Hause berechnet. Informationen zu dieser Funktion müssen an ein DATA-System eines Drittanbieters gesendet werden.

Die Geschäftslogik lautet wie folgt: Ein Artikel steht mit Lieferung zum Verkauf, wenn:

  • Produkt ist auf Lager
  • Der Auftragnehmer (zum Beispiel die Firma DostavchenKO) hat die Möglichkeit, diese zum Auftraggeber zu bringen
  • Produktfarbe - nicht blau (wir mögen kein Blau)

Unser Microservice wird über eine http-Anfrage über eine Änderung der Warenmenge im Ladenregal informiert.

Diese Benachrichtigung ist ein Auslöser für die Berechnung der Verfügbarkeit.

Plus, damit das Leben nicht Honig zu sein scheint:

  • Der Benutzer sollte in der Lage sein, bestimmte Produkte manuell zu deaktivieren.
  • Um keine Spam-Daten zu versenden, müssen Sie nur Verfügbarkeitsdaten für die Produkte senden, die sich geändert haben.

Wir lesen ein paar Mal TK - und gehen.



Integrationstest


In TDD ist eine der wichtigsten Fragen, die Sie zu allem, was Sie schreiben, stellen müssen: "Was will ich von ...?"

Und die erste Frage, die wir stellen, betrifft nur die gesamte Anwendung.
Die Frage ist also:

Was möchte ich von meinem Microservice?

Die Antwort lautet:

Eigentlich viele Dinge. Selbst eine solche einfache Logik bietet viele Optionen, ein Versuch zu schreiben, der, und noch mehr, Tests für alle zu erstellen, eine unmögliche Aufgabe sein kann. Um die Frage auf Anwendungsebene zu beantworten, werden daher nur die Haupttestfälle ausgewählt.

Das heißt, wir gehen davon aus, dass alle Eingabedaten ein gültiges Format haben, Systeme von Drittanbietern normal reagieren und zuvor keine Informationen zum Produkt vorhanden waren.

Also möchte ich:

  • Es ist eine Veranstaltung eingetroffen, bei der kein Produkt im Regal steht. Benachrichtigen Sie, dass die Lieferung nicht verfügbar ist.
  • Die Veranstaltung kam, dass das gelbe Produkt auf Lager ist, DostavchenKO ist bereit, es zu nehmen. Benachrichtigen Sie über die Verfügbarkeit von Waren.
  • Zwei Nachrichten kamen hintereinander - beide mit einer positiven Warenmenge im Laden. Nur eine Nachricht gesendet.
  • Zwei Nachrichten sind eingetroffen: In der ersten befindet sich ein Produkt im Geschäft, in der zweiten - es ist nicht mehr vorhanden. Wir senden zwei Nachrichten: zuerst - verfügbar, dann - nein.
  • Ich kann das Produkt manuell deaktivieren und es werden keine Benachrichtigungen mehr gesendet.
  • ...

Die Hauptsache hier ist, rechtzeitig anzuhalten: Wie ich bereits geschrieben habe, gibt es zu viele Optionen, und es macht keinen Sinn, alle hier zu beschreiben - nur die grundlegendsten. Wenn wir in Zukunft Tests für Geschäftslogik schreiben, wird ihre Kombination wahrscheinlich alles abdecken, was wir uns hier einfallen lassen. Die Hauptmotivation hierbei ist, sicherzustellen, dass die Anwendung bei Bedarf funktioniert, wenn diese Tests bestanden werden.

All diese Wunschliste werden wir nun in Tests destillieren. Da dies auf Anwendungsebene eine Wunschliste ist, werden wir außerdem Tests durchführen, um den Federkontext zu erhöhen, das heißt, ziemlich schwer.
Und dies leider für viele TDD-Zwecke, denn um einen solchen Integrationstest zu schreiben, benötigen Sie eine Menge Aufwand, den die Leute nicht immer bereit sind, auszugeben. Und ja, dies ist der schwierigste Schritt, aber glauben Sie mir, nachdem Sie ihn durchlaufen haben, schreibt sich der Code fast von selbst und Sie werden sicher sein, dass Ihre Anwendung genau so funktioniert, wie Sie es möchten.


Während der Beantwortung der Frage können Sie bereits mit dem Schreiben von Code in der generierten Spring-Initialisierungsklasse beginnen. Die Testnamen sind nur unsere Wunschliste. Erstellen Sie vorerst nur leere Methoden:

@Test public void notifyNotAvailableIfProductQuantityIsZero() {} @Test public void notifyAvailableYellowProductIfPositiveQuantityAndDostavchenkoApproved() {} @Test public void notifyOnceOnSeveralEqualProductMessages() {} @Test public void notifyFirstAvailableThenNotIfProductQuantityMovedFromPositiveToZero() {} @Test public void noNotificationOnDisabledProduct() {} 

In Bezug auf die Benennung von Methoden: Ich rate Ihnen dringend, sie informativ zu gestalten und nicht test1 (), test2 (), da Sie später, wenn Sie vergessen, welche Klasse Sie geschrieben haben und wofür sie verantwortlich ist, stattdessen die Möglichkeit haben Versuchen Sie, den Code direkt zu analysieren. Öffnen Sie einfach den Test und lesen Sie die Vertragsmethode, die die Klasse erfüllt.

Füllen Sie die Tests aus


Die Hauptidee ist, alles Externe zu emulieren, um zu überprüfen, was im Inneren passiert.

"Extern" in Bezug auf unseren Service ist alles, was NICHT der Microservice selbst ist, sondern der direkt mit ihm kommuniziert.

In diesem Fall ist das Äußere:

  • Das System, das unser Service über Änderungen der Warenmenge informiert
  • Kunde, der die Ware manuell trennt
  • DostavchenKO-System eines Drittanbieters

Um die Anforderungen der ersten beiden zu emulieren, verwenden wir springendes MockMvc.
Um DostavchenKO zu emulieren, verwenden wir wiremock oder MockRestServiceServer.

Daher sieht unser Integrationstest folgendermaßen aus:

Integrationstest
 @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc @AutoConfigureWireMock(port = 8090) public class TddExampleApplicationTests { @Autowired private MockMvc mockMvc; @Before public void init() { WireMock.reset(); } @Test public void notifyNotAvailableIfProductQuantityIsZero() throws Exception { stubNotification( // language=JSON "{\n" + " \"productId\": 111,\n" + " \"available\": false\n" + "}"); performQuantityUpdateRequest( // language=JSON "{\n" + " \"productId\": 111,\n" + " \"color\" : \"red\", \n" + " \"productQuantity\": 0\n" + "}"); verify(1, postRequestedFor(urlEqualTo("/notify"))); } @Test public void notifyAvailableYellowProductIfPositiveQuantityAndDostavchenkoApproved() throws Exception { stubDostavchenko("112"); stubNotification( // language=JSON "{\n" + " \"productId\": 112,\n" + " \"available\": true\n" + "}"); performQuantityUpdateRequest( // language=JSON "{\n" + " \"productId\": 112,\n" + " \"color\" : \"Yellow\", \n" + " \"productQuantity\": 10\n" + "}"); verify(1, postRequestedFor(urlEqualTo("/notify"))); } @Test public void notifyOnceOnSeveralEqualProductMessages() throws Exception { stubDostavchenko("113"); stubNotification( // language=JSON "{\n" + " \"productId\": 113,\n" + " \"available\": true\n" + "}"); for (int i = 0; i < 5; i++) { performQuantityUpdateRequest( // language=JSON "{\n" + " \"productId\": 113,\n" + " \"color\" : \"Yellow\", \n" + " \"productQuantity\": 10\n" + "}"); } verify(1, postRequestedFor(urlEqualTo("/notify"))); } @Test public void notifyFirstAvailableThenNotIfProductQuantityMovedFromPositiveToZero() throws Exception { stubDostavchenko("114"); stubNotification( // language=JSON "{\n" + " \"productId\": 114,\n" + " \"available\": true\n" + "}"); performQuantityUpdateRequest( // language=JSON "{\n" + " \"productId\": 114,\n" + " \"color\" : \"Yellow\",\n" + " \"productQuantity\": 10\n" + "}"); stubNotification( // language=JSON "{\n" + " \"productId\": 114,\n" + " \"available\": false\n" + "}"); performQuantityUpdateRequest( // language=JSON "{\n" + " \"productId\": 114,\n" + " \"color\" : \"Yellow\",\n" + " \"productQuantity\": 0\n" + "}"); verify(2, postRequestedFor(urlEqualTo("/notify"))); } @Test public void noNotificationOnDisabledProduct() throws Exception { stubNotification( // language=JSON "{\n" + " \"productId\": 115,\n" + " \"available\": false\n" + "}"); disableProduct(115); for (int i = 0; i < 5; i++) { performQuantityUpdateRequest( // language=JSON "{\n" + " \"productId\": 115,\n" + " \"color\" : \"Yellow\",\n" + " \"productQuantity\": " + i + "\n" + "}"); } verify(1, postRequestedFor(urlEqualTo("/notify"))); } private void disableProduct(int productId) throws Exception { mockMvc.perform( post("/disableProduct?productId=" + productId) ).andDo( print() ).andExpect( status().isOk() ); } private void performQuantityUpdateRequest(String content) throws Exception { mockMvc.perform( post("/product-quantity-update") .contentType(MediaType.APPLICATION_JSON) .content(content) ).andDo( print() ).andExpect( status().isOk() ); } private void stubNotification(String content) { stubFor(WireMock.post(urlEqualTo("/notify")) .withHeader("Content-Type", equalTo(MediaType.APPLICATION_JSON_UTF8_VALUE)) .withRequestBody(equalToJson(content)) .willReturn(aResponse().withStatus(HttpStatus.OK_200))); } private void stubDostavchenko(final String productId) { stubFor(get(urlEqualTo("/isDeliveryAvailable?productId=" + productId)) .willReturn(aResponse().withStatus(HttpStatus.OK_200).withBody("true"))); } } 

Was ist gerade passiert?


Wir haben einen Integrationstest geschrieben, dessen Durchgang uns die Funktionsfähigkeit des Systems gemäß den Hauptnutzergeschichten garantiert. Und wir haben es getan, bevor wir mit der Implementierung des Dienstes beginnen.

Einer der Vorteile dieses Ansatzes ist, dass ich während des Schreibprozesses zum echten DostavchenKO gehen musste, um von dort eine echte Antwort auf die echte Anfrage zu erhalten, die wir in unserem Stub gestellt haben. Es ist sehr gut, dass wir uns gleich zu Beginn der Entwicklung darum gekümmert haben und nicht, nachdem der Code geschrieben wurde. Und hier stellt sich heraus, dass das Format nicht das im TOR angegebene ist oder der Dienst im Allgemeinen nicht verfügbar ist oder etwas anderes.

Ich möchte auch darauf hinweisen, dass wir nicht nur keine einzige Codezeile geschrieben haben, die später zum Produkt führt, sondern auch nicht einmal davon ausgegangen sind, wie unser Microservice im Inneren angeordnet sein wird: Welche Schichten wird es geben, ob Wir verwenden die Basis, wenn ja, welche usw. Zum Zeitpunkt des Schreibens des Tests sind wir von der Implementierung abstrahiert, und wie wir später sehen werden, kann dies eine Reihe von architektonischen Vorteilen bringen.

Im Gegensatz zum kanonischen TDD, bei dem die Implementierung unmittelbar nach dem Test geschrieben wird, dauert der Integrationstest nicht lange. Tatsächlich wird es erst am Ende der Entwicklung grün, bis absolut alles geschrieben ist, einschließlich der Dateien.
Wir gehen weiter.

Controller


Nachdem wir den Integrationstest geschrieben haben und nun sicher sind, dass wir nach dem Bestehen der Aufgabe nachts ruhig schlafen können, ist es Zeit, mit dem Programmieren der Ebenen zu beginnen. Und die erste Schicht, die wir implementieren werden, ist der Controller. Warum genau er? Weil dies der Einstiegspunkt in das Programm ist. Wir müssen uns von oben nach unten bewegen, von der ersten Ebene, mit der der Benutzer interagieren wird, bis zur letzten.
Es ist wichtig.

Und wieder beginnt alles mit der gleichen Frage:

Was möchte ich vom Controller?

Die Antwort lautet:

Wir wissen, dass der Controller mit der Kommunikation mit dem Benutzer, der Validierung und Konvertierung von Eingabedaten befasst ist und keine Geschäftslogik enthält. Die Antwort auf diese Frage könnte also ungefähr so ​​lauten:

Ich möchte:

  • BAD_REQUEST wurde an den Benutzer zurückgegeben, wenn versucht wurde, ein Produkt mit einer ungültigen ID zu trennen
  • BAD_REQUEST beim Versuch, eine Warenänderung mit ungültiger ID zu benachrichtigen
  • BAD_REQUEST beim Versuch, eine negative Menge zu benachrichtigen
  • INTERNAL_SERVER_ERROR, wenn DostavchenKO nicht verfügbar ist
  • INTERNAL_SERVER_ERROR, wenn nicht an DATA gesendet werden kann

Da wir für alle oben genannten Elemente zusätzlich zum http-Code benutzerfreundlich sein möchten, müssen Sie eine benutzerdefinierte Nachricht anzeigen, die das Problem beschreibt, damit der Benutzer das Problem versteht.

  • 200, wenn die Verarbeitung erfolgreich war
  • INTERNAL_SERVER_ERROR mit einer Standardnachricht in allen anderen Fällen, um Stackrace nicht zu glänzen

Bis ich anfing, über TDD zu schreiben, dachte ich zuletzt darüber nach, was mein System für den Benutzer in einem speziellen und auf den ersten Blick unwahrscheinlichen Fall herausbringen würde. Ich habe aus einem einfachen Grund nicht gedacht: Das Schreiben einer Implementierung ist so schwierig, dass absolut alle Randfälle berücksichtigt werden. Manchmal ist nicht genügend RAM im Gehirn vorhanden. Und nach der schriftlichen Implementierung ist es immer noch ein Vergnügen, den Code auf etwas zu analysieren, das Sie möglicherweise nicht im Voraus in Betracht gezogen haben: Wir alle denken, dass wir sofort den perfekten Code schreiben. Es gibt zwar keine Implementierung, aber es besteht keine Notwendigkeit, darüber nachzudenken, und es gibt keine Schmerzen, dies zu ändern, wenn dies der Fall ist. Nachdem Sie den Test zuerst geschrieben haben, müssen Sie nicht warten, bis die Sterne konvergieren. Nach dem Zurückziehen zum Produkt fällt eine bestimmte Anzahl von Systemen aus, und der Kunde kommt mit der Aufforderung, etwas zu reparieren, zu Ihnen gerannt. Dies gilt nicht nur für die Steuerung.

Schreiben Sie Tests


Mit den ersten drei ist alles klar: Wir verwenden die Frühlingsvalidierung. Wenn eine ungültige Anforderung eintrifft, löst die Anwendung eine Ausnahme aus, die wir in einem Ausnahmebehandler abfangen. Hier funktioniert, wie sie sagen, alles von selbst, aber woher weiß der Controller, dass ein System eines Drittanbieters nicht verfügbar ist?

Es ist klar, dass der Controller selbst nichts über Systeme von Drittanbietern wissen sollte, weil Welches System zu fragen ist und was die Geschäftslogik ist, das heißt, es muss eine Art Vermittler geben. Dieser Vermittler ist der Dienst. Und wir werden Tests auf dem Controller schreiben, indem wir den Mock dieses Dienstes verwenden und in bestimmten Fällen sein Verhalten emulieren. Der Dienst muss den Controller also irgendwie darüber informieren, dass das System nicht verfügbar ist. Sie können dies auf verschiedene Arten tun, aber am einfachsten, um eine benutzerdefinierte Ausführung zu starten. Wir werden einen Test für dieses Controller-Verhalten schreiben.

Test auf Kommunikationsfehler mit einem DATA-System eines Drittanbieters
 @RunWith(SpringRunner.class) @WebMvcTest @AutoConfigureMockMvc public class ControllerTest { @MockBean private UpdateProcessorService updateProcessorService; @Test public void returnServerErrorOnDataCommunicationError() throws Exception { doThrow(new DataCommunicationException()).when(updateProcessorService).processUpdate(any(Update.class)); performUpdate( //language=JSON "{\n" + " \"productId\": 1,\n" + " \"color\": \"red\",\n" + " \"productQuantity\": 10\n" + "}" ).andDo( print() ).andExpect( status().isInternalServerError() ).andExpect( content().json("{\n" + " \"errors\": [\n" + " {\n" + " \"message\": \"Can't communicate with Data system\"\n" + " }\n" + " ]\n" + "}") ); } } 


Zu diesem Zeitpunkt erschienen mehrere Dinge für sich:

  • Ein Service, der in die Steuerung eingespeist wird und an den die Verarbeitung einer eingehenden Nachricht für eine neue Warenmenge delegiert wird.
  • Die Methode dieses Dienstes und dementsprechend seine Unterschrift, die diese Verarbeitung durchführt.
  • Die Erkenntnis, dass die Methode eine benutzerdefinierte Ausführung auslösen sollte, wenn das System nicht verfügbar ist.
  • Diese benutzerdefinierte Ausführung selbst.

Warum alleine? Denn wie Sie sich erinnern, haben wir noch keine Implementierung geschrieben. Und all diese Entitäten tauchten bei der Programmierung von Tests auf. Damit der Compiler nicht schwört, müssen wir in echtem Code alles erstellen, was oben beschrieben wurde. Glücklicherweise hilft uns fast jede IDE dabei, die erforderlichen Entitäten zu generieren. Wir schreiben also eine Art Test - und die Anwendung ist mit Klassen und Methoden gefüllt.

Insgesamt sind die Tests für die Steuerung wie folgt:

Tests
 @RunWith(SpringRunner.class) @WebMvcTest @AutoConfigureMockMvc public class ControllerTest { @InjectMocks private Controller controller; @MockBean private UpdateProcessorService updateProcessorService; @Autowired private MockMvc mvc; @Test public void returnBadRequestOnDisableWithInvalidProductId() throws Exception { mvc.perform( post("/disableProduct?productId=-443") ).andDo( print() ).andExpect( status().isBadRequest() ).andExpect( content().json(getInvalidProductIdJsonContent()) ); } @Test public void returnBadRequestOnNotifyWithInvalidProductId() throws Exception { performUpdate( //language=JSON "{\n" + " \"productId\": -1,\n" + " \"color\": \"red\",\n" + " \"productQuantity\": 0\n" + "}" ).andDo( print() ).andExpect( status().isBadRequest() ).andExpect( content().json(getInvalidProductIdJsonContent()) ); } @Test public void returnBadRequestOnNotifyWithNegativeProductQuantity() throws Exception { performUpdate( //language=JSON "{\n" + " \"productId\": 1,\n" + " \"color\": \"red\",\n" + " \"productQuantity\": -10\n" + "}" ).andDo( print() ).andExpect( status().isBadRequest() ).andExpect( content().json("{\n" + " \"errors\": [\n" + " {\n" + " \"message\": \"productQuantity is invalid\"\n" + " }\n" + " ]\n" + "}") ); } @Test public void returnServerErrorOnDostavchenkoCommunicationError() throws Exception { doThrow(new DostavchenkoException()).when(updateProcessorService).processUpdate(any(Update.class)); performUpdate( //language=JSON "{\n" + " \"productId\": 1,\n" + " \"color\": \"red\",\n" + " \"productQuantity\": 10\n" + "}" ).andDo( print() ).andExpect( status().isInternalServerError() ).andExpect( content().json("{\n" + " \"errors\": [\n" + " {\n" + " \"message\": \"DostavchenKO communication exception\"\n" + " }\n" + " ]\n" + "}") ); } @Test public void returnServerErrorOnDataCommunicationError() throws Exception { doThrow(new DataCommunicationException()).when(updateProcessorService).processUpdate(any(Update.class)); performUpdate( //language=JSON "{\n" + " \"productId\": 1,\n" + " \"color\": \"red\",\n" + " \"productQuantity\": 10\n" + "}" ).andDo( print() ).andExpect( status().isInternalServerError() ).andExpect( content().json("{\n" + " \"errors\": [\n" + " {\n" + " \"message\": \"Can't communicate with Data system\"\n" + " }\n" + " ]\n" + "}") ); } @Test public void return200OnSuccess() throws Exception { performUpdate( //language=JSON "{\n" + " \"productId\": 1,\n" + " \"color\": \"red\",\n" + " \"productQuantity\": 10\n" + "}" ).andDo( print() ).andExpect( status().isOk() ); } @Test public void returnServerErrorOnUnexpectedException() throws Exception { doThrow(new RuntimeException()).when(updateProcessorService).processUpdate(any(Update.class)); performUpdate( //language=JSON "{\n" + " \"productId\": 1,\n" + " \"color\": \"red\",\n" + " \"productQuantity\": 10\n" + "}" ).andDo( print() ).andExpect( status().isInternalServerError() ).andExpect( content().json("{\n" + " \"errors\": [\n" + " {\n" + " \"message\": \"Internal Server Error\"\n" + " }\n" + " ]\n" + "}") ); } @Test public void returnTwoErrorMessagesOnInvalidProductIdAndNegativeQuantity() throws Exception { performUpdate( //language=JSON "{\n" + " \"productId\": -1,\n" + " \"color\": \"red\",\n" + " \"productQuantity\": -10\n" + "}" ).andDo( print() ).andExpect( status().isBadRequest() ).andExpect( content().json("{\n" + " \"errors\": [\n" + " { \"message\": \"productQuantity is invalid\" },\n" + " { \"message\": \"productId is invalid\" }\n" + " ]\n" + "}") ); } private ResultActions performUpdate(String jsonContent) throws Exception { return mvc.perform( post("/product-quantity-update") .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE) .content(jsonContent) ); } private String getInvalidProductIdJsonContent() { return //language=JSON "{\n" + " \"errors\": [\n" + " {\n" + " \"message\": \"productId is invalid\"\n" + " }\n" + " ]\n" + "}"; } } 

Jetzt können wir die Implementierung schreiben und sicherstellen, dass alle Tests erfolgreich bestanden wurden:
Implementierung
 @RestController @AllArgsConstructor @Validated @Slf4j public class Controller { private final UpdateProcessorService updateProcessorService; @PostMapping("/product-quantity-update") public void updateQuantity(@RequestBody @Valid Update update) { updateProcessorService.processUpdate(update); } @PostMapping("/disableProduct") public void disableProduct(@RequestParam("productId") @Min(0) Long productId) { updateProcessorService.disableProduct(Long.valueOf(productId)); } } 


Ausnahmehandler
 @ControllerAdvice @Slf4j public class ApplicationExceptionHandler { @ExceptionHandler(ConstraintViolationException.class) @ResponseBody @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse onConstraintViolationException(ConstraintViolationException exception) { log.info("Constraint Violation", exception); return new ErrorResponse(exception.getConstraintViolations().stream() .map(constraintViolation -> new ErrorResponse.Message( ((PathImpl) constraintViolation.getPropertyPath()).getLeafNode().toString() + " is invalid")) .collect(Collectors.toList())); } @ExceptionHandler(value = MethodArgumentNotValidException.class) @ResponseBody @ResponseStatus(value = HttpStatus.BAD_REQUEST) public ErrorResponse onMethodArgumentNotValidException(MethodArgumentNotValidException exception) { log.info(exception.getMessage()); List<ErrorResponse.Message> fieldErrors = exception.getBindingResult().getFieldErrors().stream() .map(fieldError -> new ErrorResponse.Message(fieldError.getField() + " is invalid")) .collect(Collectors.toList()); return new ErrorResponse(fieldErrors); } @ExceptionHandler(DostavchenkoException.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse onDostavchenkoCommunicationException(DostavchenkoException exception) { log.error("DostavchenKO communication exception", exception); return new ErrorResponse(Collections.singletonList( new ErrorResponse.Message("DostavchenKO communication exception"))); } @ExceptionHandler(DataCommunicationException.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse onDataCommunicationException(DataCommunicationException exception) { log.error("DostavchenKO communication exception", exception); return new ErrorResponse(Collections.singletonList( new ErrorResponse.Message("Can't communicate with Data system"))); } @ExceptionHandler(Exception.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse onException(Exception exception) { log.error("Error while processing", exception); return new ErrorResponse(Collections.singletonList( new ErrorResponse.Message(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()))); } } 


Was ist gerade passiert?


In TDD müssen Sie nicht den gesamten Code im Kopf behalten.

Lassen Sie uns noch einmal sagen: Behalten Sie nicht die gesamte Architektur im RAM. Schauen Sie sich nur eine Schicht an. Er ist einfach.

Im üblichen Prozess reicht das Gehirn nicht aus, da es viele Implementierungen gibt. Wenn Sie ein Superheld sind, der alle Nuancen eines großen Projekts in Ihrem Kopf berücksichtigen kann, ist TDD nicht erforderlich. Ich weiß nicht wie. Je größer das Projekt, desto mehr irre ich mich.

Nachdem Sie erkannt haben, dass Sie nur verstehen müssen, was die nächste Schicht benötigt, kommt die Erleuchtung ins Leben. Tatsache ist, dass Sie mit diesem Ansatz keine unnötigen Dinge tun können. Hier sprichst du mit einem Mädchen. Sie sagt etwas über ein Problem bei der Arbeit. Und Sie denken, wie Sie es lösen können, Sie zerbrechen sich den Kopf. Und sie muss es nicht lösen, sie muss es nur sagen. Und alle. Sie wollte nur etwas teilen. Es ist von unschätzbarem Wert, dies in der ersten Phase von listen () zu lernen. Für alles andere ... na ja, wissen Sie.


Service


Als nächstes implementieren wir den Service.

Was wollen wir vom Service?

Wir möchten, dass er sich mit Geschäftslogik befasst, d.h.

  1. Er wusste, wie man Waren trennt, und benachrichtigte auch über :
  2. Verfügbarkeit, wenn das Produkt nicht getrennt ist, auf Lager ist, die Farbe des Produkts gelb ist und DostavchenKO zur Lieferung bereit ist.
  3. Unzugänglichkeit, wenn die Ware unabhängig von irgendetwas nicht verfügbar ist.
  4. Unzugänglichkeit, wenn das Produkt blau ist.
  5. Unzugänglichkeit, wenn DostavchenKO sich weigert, es zu tragen.
  6. Unzugänglichkeit, wenn die Ware manuell getrennt wird.
  7. Als Nächstes soll der Dienst die Ausführung auslösen, wenn eines der Systeme nicht verfügbar ist.
  8. Und um keine DATEN zu spammen, müssen Sie auch das verzögerte Senden von Nachrichten organisieren, nämlich:
  9. Wenn wir früher verfügbare Waren für Waren verschickt haben und jetzt berechnet haben, was verfügbar ist, dann senden wir nichts.
  10. Und wenn es vorher nicht verfügbar ist, aber jetzt verfügbar ist, senden wir es.
  11. Und du musst es irgendwo aufschreiben ...

STOP!


Denken Sie nicht, dass unser Service zu viel zu tun hat?

Nach unserer Wunschliste zu urteilen, weiß er, wie man Waren abschaltet, berücksichtigt die Zugänglichkeit und stellt sicher, dass er keine zuvor gesendeten Nachrichten sendet. Dies ist kein hoher Zusammenhalt. Es ist notwendig, heterogene Funktionalitäten in verschiedene Klassen zu verschieben, und daher sollte es bereits drei Dienste geben: Der eine befasst sich mit der Trennung von Waren, der andere berechnet die Möglichkeit der Lieferung und gibt sie an einen Dienst weiter, der entscheidet, ob er gesendet wird oder nicht. Auf diese Weise weiß der Geschäftslogikdienst übrigens nichts über das DATA-System, was ebenfalls ein klares Plus ist.

Nach meiner Erfahrung ist es oftmals leicht, architektonische Momente zu übersehen, nachdem ich mich intensiv mit der Implementierung befasst habe. Wenn wir den Service sofort schreiben würden, ohne darüber nachzudenken, was er tun soll, und was noch wichtiger ist, als es NICHT sein sollte, würde sich die Wahrscheinlichkeit erhöhen, dass sich Verantwortungsbereiche überschneiden. Ich möchte in meinem eigenen Namen hinzufügen, dass mir dieses Beispiel in der Praxis passiert ist und der qualitative Unterschied zwischen den Ergebnissen von TDD und sequentiellen Programmieransätzen mich dazu inspiriert hat, diesen Beitrag zu schreiben.

Geschäftslogik


Wenn wir aus den gleichen Gründen wie dem hohen Zusammenhalt über den Geschäftslogikdienst nachdenken, verstehen wir, dass wir eine weitere Abstraktionsebene zwischen ihm und dem echten DostavchenKO benötigen. Und da wir den Service zuerst entwerfen, können wir vom DostavchenKO-Kunden einen solchen internen Vertrag verlangen, den wir wollen. Beim Schreiben eines Tests für die Geschäftslogik werden wir verstehen, was wir vom Kunden mit der folgenden Signatur erwarten:

 public boolean isAvailableForTransportation(Long productId) {...} 

Auf der Service-Ebene spielt es für uns keine Rolle, wie der echte DostavchenKO antwortet: In Zukunft wird die Aufgabe des Kunden diese Informationen irgendwie aus ihm herausholen. Einmal mag es einfach sein, aber manchmal wird es notwendig sein, mehrere Anfragen zu stellen: Im Moment sind wir davon abstrahiert.

Wir möchten eine ähnliche Unterschrift von einem Dienst, der sich mit nicht verbundenen Waren befasst:

 public boolean isProductEnabled(Long productId) {...} 

Die Fragen „Was möchte ich vom Geschäftslogikdienst?“, Die in den Tests aufgezeichnet wurden, sehen also wie folgt aus:

Servicetests
 @RunWith(MockitoJUnitRunner.class) public class UpdateProcessorServiceTest { @InjectMocks private UpdateProcessorService updateProcessorService; @Mock private ManualExclusionService manualExclusionService; @Mock private DostavchenkoClient dostavchenkoClient; @Mock private AvailabilityNotifier availabilityNotifier; @Test public void notifyAvailableIfYellowProductIsEnabledAndReadyForTransportation() { final Update testProduct = new Update(1L, 10L, "Yellow"); when(dostavchenkoClient.isAvailableForTransportation(testProduct.getProductId())).thenReturn(true); when(manualExclusionService.isProductEnabled(testProduct.getProductId())).thenReturn(true); updateProcessorService.processUpdate(testProduct); verify(availabilityNotifier, only()).notify(eq(new ProductAvailability(testProduct.getProductId(), true))); } @Test public void notifyNotAvailableIfProductIsAbsent() { final Update testProduct = new Update(1L, 0L, "Yellow"); updateProcessorService.processUpdate(testProduct); verify(availabilityNotifier, only()).notify(eq(new ProductAvailability(testProduct.getProductId(), false))); verifyNoMoreInteractions(manualExclusionService); verifyNoMoreInteractions(dostavchenkoClient); } @Test public void notifyNotAvailableIfProductIsBlue() { final Update testProduct = new Update(1L, 10L, "Blue"); updateProcessorService.processUpdate(testProduct); verify(availabilityNotifier, only()).notify(eq(new ProductAvailability(testProduct.getProductId(), false))); verifyNoMoreInteractions(manualExclusionService); verifyNoMoreInteractions(dostavchenkoClient); } @Test public void notifyNotAvailableIfProductIsDisabled() { final Update testProduct = new Update(1L, 10L, "Yellow"); when(manualExclusionService.isProductEnabled(testProduct.getProductId())).thenReturn(false); updateProcessorService.processUpdate(testProduct); verify(availabilityNotifier, only()).notify(eq(new ProductAvailability(testProduct.getProductId(), false))); verifyNoMoreInteractions(dostavchenkoClient); } @Test public void notifyNotAvailableIfProductIsNotReadyForTransportation() { final Update testProduct = new Update(1L, 10L, "Yellow"); when(dostavchenkoClient.isAvailableForTransportation(testProduct.getProductId())).thenReturn(false); when(manualExclusionService.isProductEnabled(testProduct.getProductId())).thenReturn(true); updateProcessorService.processUpdate(testProduct); verify(availabilityNotifier, only()).notify(eq(new ProductAvailability(testProduct.getProductId(), false))); } @Test(expected = DostavchenkoException.class) public void throwCustomExceptionIfDostavchenkoCommunicationFailed() { final Update testProduct = new Update(1L, 10L, "Yellow"); when(dostavchenkoClient.isAvailableForTransportation(testProduct.getProductId())) .thenThrow(new RestClientException("Something's wrong")); when(manualExclusionService.isProductEnabled(testProduct.getProductId())).thenReturn(true); updateProcessorService.processUpdate(testProduct); } } 


Zu diesem Zeitpunkt wurden sie von selbst geboren:

  • DostavchenKO Kunde mit servicefreundlicher Singatura
  • Ein Dienst, bei dem die Logik des verzögerten Sendens implementiert werden muss, an den der entworfene Dienst die Ergebnisse seiner Arbeit übermittelt
  • Service von nicht verbundenen Waren und deren Unterschrift

Implementierung:

Implementierung
 @RequiredArgsConstructor @Service @Slf4j public class UpdateProcessorService { private final AvailabilityNotifier availabilityNotifier; private final DostavchenkoClient dostavchenkoClient; private final ManualExclusionService manualExclusionService; public void processUpdate(Update update) { if (update.getProductQuantity() <= 0) { availabilityNotifier.notify(getNotAvailableProduct(update.getProductId())); return; } if ("Blue".equals(update.getColor())) { availabilityNotifier.notify(getNotAvailableProduct(update.getProductId())); return; } if (!manualExclusionService.isProductEnabled(update.getProductId())) { availabilityNotifier.notify(getNotAvailableProduct(update.getProductId())); return; } try { final boolean availableForTransportation = dostavchenkoClient.isAvailableForTransportation(update.getProductId()); availabilityNotifier.notify(new ProductAvailability(update.getProductId(), availableForTransportation)); } catch (Exception exception) { log.warn("Problems communicating with DostavchenKO", exception); throw new DostavchenkoException(); } } private ProductAvailability getNotAvailableProduct(Long productId) { return new ProductAvailability(productId, false); } } 


Produkte deaktivieren


Es ist Zeit für eine der unvermeidlichen TDD-Phasen - das Refactoring. Wenn Sie sich erinnern, sah der Servicevertrag nach der Implementierung des Controllers folgendermaßen aus:

 public void disableProduct(long productId) 

Und jetzt haben wir beschlossen, die Trennungslogik in einen separaten Dienst zu verschieben.

Von diesem Service wollen wir zu diesem Zeitpunkt Folgendes:

  • Die Fähigkeit, Waren auszuschalten.
  • Wir möchten, dass er zurücksendet, dass die Ware getrennt wird, wenn er zuvor getrennt wurde.
  • Wir möchten, dass er zurücksendet, dass die Ware verfügbar ist, wenn zuvor keine Unterbrechung aufgetreten ist.

Mit Blick auf die Wunschliste, die eine direkte Folge des Vertrags zwischen dem Geschäftslogikdienst und dem geplanten Dienst ist, möchte ich Folgendes beachten:

  1. Erstens können Sie sofort erkennen, dass die Anwendung möglicherweise Probleme hat, wenn jemand das nicht verbundene Produkt wieder ausschalten möchte, da dieser Dienst derzeit einfach nicht weiß, wie dies zu tun ist. Und das bedeutet, dass es sich vielleicht lohnt, dieses Problem mit dem Analysten zu besprechen, der die Aufgabe für die Entwicklung festgelegt hat. Ich verstehe, dass in diesem Fall diese Frage unmittelbar nach der ersten Lesung des ToR hätte gestellt werden müssen, aber wir entwerfen ein ziemlich einfaches System. In größeren Projekten ist dies möglicherweise nicht so offensichtlich. Darüber hinaus wussten wir nicht, dass wir eine Einheit haben würden, die nur für die Funktionalität der Trennung von Waren verantwortlich ist: Ich erinnere mich, dass wir nur im Entwicklungsprozess geboren wurden.
  2. -, . — , . , , , , , , . . ProductAvailability. , . . ., , god object, , , , TDD, , . , , «» — : « ...» , , TDD, .

Tests und Implementierung sind sehr einfach:

Tests
 @SpringBootTest @RunWith(SpringRunner.class) public class ManualExclusionServiceTest { @Autowired private ManualExclusionService service; @Autowired private ManualExclusionRepository manualExclusionRepository; @Before public void clearDb() { manualExclusionRepository.deleteAll(); } @Test public void disableItem() { Long productId = 100L; service.disableProduct(productId); assertThat(service.isProductEnabled(productId), is(false)); } @Test public void returnEnabledIfProductWasNotDisabled() { assertThat(service.isProductEnabled(100L), is(true)); assertThat(service.isProductEnabled(200L), is(true)); } } 


Implementierung
 @Service @AllArgsConstructor public class ManualExclusionService { private final ManualExclusionRepository manualExclusionRepository; public boolean isProductEnabled(Long productId) { return !manualExclusionRepository.exists(productId); } public void disableProduct(long productId) { manualExclusionRepository.save(new ManualExclusion(productId)); } } 


Lazy Submission Service


Wir sind also zum letzten Dienst gekommen, der sicherstellt, dass das DATA-System nicht mit denselben Nachrichten gespammt wird.

Ich möchte Sie daran erinnern, dass das Ergebnis der Arbeit des Geschäftslogikdienstes, dh das ProductAvailability-Objekt, in dem nur zwei Felder vorhanden sind: productId und isAvailable, bereits darauf übertragen wurde.

Nach der guten alten Tradition beginnen wir darüber nachzudenken, was wir von diesem Service erwarten:

  • In jedem Fall zum ersten Mal eine Benachrichtigung senden.
  • Senden einer Benachrichtigung, wenn sich die Verfügbarkeit des Produkts geändert hat.
  • Wir senden nichts, wenn nicht.
  • Wenn das Senden an ein Drittanbieter-System mit einer Ausnahme beendet wurde, sollte die Benachrichtigung, die die Ausnahme verursacht hat, nicht in die Datenbank der gesendeten Benachrichtigungen aufgenommen werden.
  • Bei der Ausführung von der DATA-Seite muss der Dienst außerdem seine DataCommunicationException auslösen.

Hier ist alles relativ einfach, aber ich möchte einen Punkt erwähnen:

Wir benötigen Informationen über das, was wir zuvor gesendet haben, was bedeutet, dass wir ein Repository haben, in dem wir frühere Berechnungen zur Verfügbarkeit von Waren speichern.

Das ProductAvailability-Objekt eignet sich nicht zum Speichern, da zumindest keine Kennung vorhanden ist. Daher ist es logisch, eine weitere zu erstellen. Die Hauptsache hier ist, nicht auszuflippen und diesen Bezeichner nicht zusammen mit @Document (wir werden MongoDb als Basis verwenden) und Indizes in ProductAvailability selbst hinzuzufügen.

Sie müssen verstehen, dass das ProductAvailability-Objekt mit allen wenigen Feldern in der Phase des Entwurfs von Klassen erstellt wurde, die in der Aufrufhierarchie höher sind als die, die wir derzeit entwerfen. Diese Klassen müssen nichts über datenbankspezifische Felder wissen, da diese Informationen beim Entwerfen nicht erforderlich waren.

Aber das ist alles Gerede.

Interessanterweise bedeutet das Hinzufügen neuer Felder dazu, dass diese Tests auch überarbeitet werden müssen, was möglicherweise einige Anstrengungen erfordert, da wir bereits eine Reihe von Tests mit der ProductAvailability geschrieben haben, die wir jetzt auf den Service übertragen. Dies bedeutet, dass es viel weniger Menschen geben wird, die aus ProductAvailability ein Gottobjekt machen möchten, als wenn sie die Implementierung sofort geschrieben hätten: Im Gegenteil, das Hinzufügen eines Felds zu einem vorhandenen Objekt wäre einfacher als das Erstellen einer anderen Klasse.

Tests
 @RunWith(SpringRunner.class) @SpringBootTest public class LazyAvailabilityNotifierTest { @Autowired private LazyAvailabilityNotifier lazyAvailabilityNotifier; @MockBean @Qualifier("dataClient") private AvailabilityNotifier availabilityNotifier; @Autowired private AvailabilityRepository availabilityRepository; @Before public void clearDb() { availabilityRepository.deleteAll(); } @Test public void notifyIfFirstTime() { sendNotificationAndVerifyDataBase(new ProductAvailability(1L, false)); } @Test public void notifyIfAvailabilityChanged() { final ProductAvailability oldProductAvailability = new ProductAvailability(1L, false); sendNotificationAndVerifyDataBase(oldProductAvailability); final ProductAvailability newProductAvailability = new ProductAvailability(1L, true); sendNotificationAndVerifyDataBase(newProductAvailability); } @Test public void doNotNotifyIfAvailabilityDoesNotChanged() { final ProductAvailability productAvailability = new ProductAvailability(1L, false); sendNotificationAndVerifyDataBase(productAvailability); sendNotificationAndVerifyDataBase(productAvailability); sendNotificationAndVerifyDataBase(productAvailability); sendNotificationAndVerifyDataBase(productAvailability); verify(availabilityNotifier, only()).notify(eq(productAvailability)); } @Test public void doNotSaveIfSentWithException() { doThrow(new RuntimeException()).when(availabilityNotifier).notify(anyObject()); boolean exceptionThrown = false; try { availabilityNotifier.notify(new ProductAvailability(1L, false)); } catch (RuntimeException exception) { exceptionThrown = true; } assertTrue("Exception was not thrown", exceptionThrown); assertThat(availabilityRepository.findAll(), hasSize(0)); } @Test(expected = DataCommunicationException.class) public void wrapDataException() { doThrow(new RestClientException("Something wrong")).when(availabilityNotifier).notify(anyObject()); lazyAvailabilityNotifier.notify(new ProductAvailability(1L, false)); } private void sendNotificationAndVerifyDataBase(ProductAvailability productAvailability) { lazyAvailabilityNotifier.notify(productAvailability); verify(availabilityNotifier).notify(eq(productAvailability)); assertThat(availabilityRepository.findAll(), hasSize(1)); assertThat(availabilityRepository.findAll().get(0), hasProperty("productId", is(productAvailability.getProductId()))); assertThat(availabilityRepository.findAll().get(0), hasProperty("availability", is(productAvailability.isAvailable()))); } } 


Implementierung
 @Component @AllArgsConstructor @Slf4j public class LazyAvailabilityNotifier implements AvailabilityNotifier { private final AvailabilityRepository availabilityRepository; private final AvailabilityNotifier availabilityNotifier; @Override public void notify(ProductAvailability productAvailability) { final AvailabilityPersistenceObject persistedProductAvailability = availabilityRepository .findByProductId(productAvailability.getProductId()); if (persistedProductAvailability == null) { notifyWith(productAvailability); availabilityRepository.save(createObjectFromProductAvailability(productAvailability)); } else if (persistedProductAvailability.isAvailability() != productAvailability.isAvailable()) { notifyWith(productAvailability); persistedProductAvailability.setAvailability(productAvailability.isAvailable()); availabilityRepository.save(persistedProductAvailability); } } private void notifyWith(ProductAvailability productAvailability) { try { availabilityNotifier.notify(productAvailability); } catch (RestClientException exception) { log.error("Couldn't notify", exception); throw new DataCommunicationException(); } } private AvailabilityPersistenceObject createObjectFromProductAvailability(ProductAvailability productAvailability) { return new AvailabilityPersistenceObject(productAvailability.getProductId(), productAvailability.isAvailable()); } } 


Fazit



Ein ähnlicher Antrag musste in der Praxis geschrieben werden. Und es stellte sich heraus, dass es zuerst ohne TDD geschrieben wurde, dann sagte das Unternehmen, dass es nicht notwendig sei, und nach sechs Monaten änderten sich die Anforderungen, und es wurde beschlossen, es erneut von Grund auf neu zu schreiben (der Vorteil ist die Microservice-Architektur, und es war nicht so beängstigend, etwas wegzuwerfen). .

Wenn ich dieselbe Anwendung mit unterschiedlichen Techniken schreibe, kann ich ihre Unterschiede erkennen. In meiner Praxis habe ich gesehen, wie TDD beim Aufbau der Architektur hilft, scheint mir korrekter.

Ich kann davon ausgehen, dass der Grund dafür nicht die Erstellung von Tests vor der Implementierung ist, sondern dass wir, nachdem wir die Tests zu Beginn geschrieben haben, zunächst darüber nachdenken, was die erstellte Klasse tun wird. Auch wenn es keine Implementierung gibt, können wir in den aufgerufenen Objekten wirklich den genauen Vertrag „ordnen“, den das Objekt, das sie aufruft, benötigt, ohne die Versuchung zu haben, schnell irgendwo etwas hinzuzufügen und eine Entität zu erhalten, die viele Aufgaben gleichzeitig erledigt.

Als einer der Hauptvorteile von TDD für mich selbst kann ich hervorheben, dass ich wirklich mehr Vertrauen in das Produkt habe, das ich produziere. Dies kann auf die Tatsache zurückzuführen sein, dass der durchschnittliche Code, der auf TDD geschrieben wurde, wahrscheinlich besser durch Tests abgedeckt wird. Nachdem ich jedoch mit dem Schreiben auf TDD begonnen hatte, wurde meine Anzahl der Änderungen am Code nach dem Geben reduziert seine Prüfung fast auf Null.

Und im Allgemeinen hatte ich das Gefühl, als Entwickler besser zu werden.

Anwendungscode finden Sie hier . Für diejenigen, die verstehen möchten, wie es in Schritten erstellt wurde, empfehle ich, auf die Historie der Commits zu achten, nachdem sie analysiert haben, was hoffentlich den Prozess der Erstellung einer typischen TDD-Anwendung verständlicher macht.

Hier ist eine sehr nützlicheEin Video , das ich jedem empfehlen kann, der in die Welt von TDD eintauchen möchte.

Der Anwendungscode verwendet eine formatierte Zeichenfolge wie json wieder. Dies ist erforderlich, um zu überprüfen, wie die Anwendung json auf POJO-Objekten analysiert. Wenn Sie IDEA verwenden, kann die erforderliche Formatierung mithilfe von JSON-Sprachinjektionen schnell und problemlos erreicht werden.

Was sind die Nachteile des Ansatzes?


Es ist eine lange Zeit, sich zu entwickeln. Mein Kollege, der im Standardparadigma programmiert, könnte es sich leisten, den Service den Testern zum Testen ohne Tests zur Verfügung zu stellen und sie auf dem Weg hinzuzufügen. Es war sehr schnell. Auf TDD funktioniert dies nicht. Wenn Sie enge Fristen haben, sind Ihre Manager unglücklich. Hier ist der Kompromiss zwischen sofort gut, aber lange und nicht sehr gut, aber schnell. Ich wähle die erste für mich, weil die zweite dadurch länger ist. Und mit großen Nerven.

Meiner Meinung nach ist TDD nicht geeignet, wenn Sie viel umgestalten müssen: Im Gegensatz zu einer von Grund auf neu erstellten Anwendung ist es nicht offensichtlich, wie Sie vorgehen und was Sie zuerst tun sollen. Es kann sich herausstellen, dass Sie an einem Klassentest arbeiten, wodurch dieser gelöscht wird.

TDD ist keine Silberkugel. Dies ist eine Geschichte über klaren, lesbaren Code, der Leistungsprobleme verursachen kann. Sie haben beispielsweise N Klassen erstellt, die wie in Fowler jeweils ihre eigenen Aufgaben ausführen. Und dann stellt sich heraus, dass jeder, um seine Arbeit zu erledigen, zur Basis gehen muss. Und Sie werden N Abfragen in der Datenbank haben. Anstatt zum Beispiel 1 Gottobjekt zu machen und 1 Mal zu gehen. Wenn Sie um Millisekunden kämpfen, müssen Sie dies bei Verwendung von TDD berücksichtigen: Der lesbare Code ist nicht immer der schnellste.

Und schließlich ist es ziemlich schwierig, auf diese Methode umzusteigen - Sie müssen sich selbst beibringen, anders zu denken. Der größte Teil der Schmerzen befindet sich im ersten Stadium. Den ersten Integrationstest habe ich 1,5 Tage geschrieben.

Nun, der letzte. Wenn Sie TDD verwenden und Ihr Code immer noch nicht sehr istDann ist die Angelegenheit möglicherweise nicht in der Methodik enthalten. Aber es hat mir geholfen.

Source: https://habr.com/ru/post/de456662/


All Articles