How to save on a psychotherapist using test-driven development

How to save on a psychotherapist using test-driven development


Have you ever had this condition?

image
< br/> I want to show you how TDD can improve the quality of the code with a specific example.
Because everything that I met while studying the issue was rather theoretical.
It so happened that I happened to write two almost identical applications: one was written in the classical style, since I did not know TDD at that time, and the second was using TDD.

Below, I will show you where the biggest differences were.

Personally, it was important to me, because every time someone found a bug in my code, I caught a weighty minus to self-esteem. Yes, I understood that bugs are normal, they are written by everyone, but the feeling of inferiority did not go anywhere. Also, in the process of the evolution of the service, I sometimes realized that I had written it myself that itching to throw everything out and rewrite everything. And how it happened is not clear. Somehow, everything was good at the beginning, but you will not look at a couple of features and after a while already without looking at the architecture. Although it seems every step of change was logical. The feeling that I do not like the product of my own work smoothly flowed into the feeling that the programmer is from me, forgive me, as if from a shit bullet.

It turned out that I am not the only one and many of my colleagues have similar feelings. And then I decided that I would either learn to write normally, or it was time to change my profession. I tried test-driven development in an attempt to change something in my approach to programming.

Looking ahead, according to the results of several projects, I can say that TDD provides a cleaner architecture, but at the same time slows down the development. And not always suitable and not for everyone.

What is TDD again


image


TDD - development through testing. Wiki article here .
The classic approach is to first write the application, then cover it with tests.

TDD-approach - first we write tests for a class, then implementation. We move along the levels of abstraction - from the highest to the applied, simultaneously breaking the application into layers-classes, from which we order the behavior we need, being free from a concrete implementation.

And, if I read it for the first time, I would not understand anything either.
Too many abstract words: let's look at an example.
We will write a real spring application in Java, we will write it on TDD, and I will try to show my thinking process during the development process and finally draw conclusions about whether it makes sense to spend time on TDD or not.

Practical challenge


Suppose we are so lucky that we have the TK of what we need to develop. Usually analysts don't bother with him, and it looks like this:

It is necessary to develop a microservice that will count on the possibility of selling goods with the subsequent delivery to the client at home. Information about this feature should be sent to a third-party DATA system

The business logic is as follows: the item is available for sale with delivery if:

  • Goods are in stock
  • The contractor (for example, the company DostavchenKO) has the opportunity to take him to the client
  • The color of the product is not blue (do not like blue)

Our microservice will be notified about the change in the quantity of goods on the store shelf via an http request.

This notification is a trigger to the calculation of accessibility.

Plus, so that life does not seem honey:

  • The user must have the ability to manually disable some products.
  • To avoid spamming DATA, you need to send only availability data for products that have changed.

We read a couple of times TK - and go.



Integration Test


In TDD, one of the most important questions that will have to be asked to everything that you write is: “What do I want from ...?”

And we ask the first question for the entire application.
So the question is:

What do I want from my microservice?

Answer:

In fact, a lot of things. Even such simple logic gives a lot of options, an attempt to write down which, and even more so to create tests for all of them, can be an impossible task. Therefore, to answer the question at the application level, we will select only the main test cases.

That is, we assume that all the input data are in a valid format, third-party systems are responding normally, and there was no information previously on the product.

So, I want to:

  • An event has arrived that there is no product on the shelf. We notify you that delivery is not available.
  • The event came that the yellow goods are in stock, DostavchenKO is ready to take it. We notify you of the availability of the product.
  • There were two consecutive messages - both with a positive amount of goods in the store. Only sent one message.
  • Two messages have arrived: in the first one there is a product in the store, in the second one - no longer. We send two messages: first - available, then - no.
  • I can turn off a product manually, and notifications are no longer sent to it.
  • ...

The main thing here is to stop in time: as I already wrote, there are too many options, and it makes no sense to describe all of them here - only the most are basic. In the future, when we write tests on business logic, their totality is likely to cover everything that we come up with here. The main motivation here is to be sure that if these tests pass, then the application works as we need.

All these Wishlist we will now distill into tests. Moreover, since this is an application-level Wishlist, then we will have tests with raising the context, i.e., quite heavy ones.
And with this, unfortunately, for many TDD ends, since in order to write such an integration test, you need quite a lot of effort that people are not always willing to spend. And yes, this is the most difficult step, but believe me, after you pass it, the code will almost write itself later, and you will be sure that your application will work exactly as you want.


In the process of answering the question, you can already start writing code in the generated spring initializr class. The names of the tests - this is just our Wishlist. For now, just create empty methods:

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

Regarding the naming of methods: I strongly advise you to make them informative, and not test1 (), test2 (), since later, when you forget what class you wrote and what it answers, you will have the opportunity instead try to parse the code directly, just open the test and read the methods of the contract to which the class satisfies.

Starting to fill out tests


The basic idea is to emulate everything external to check what is going on inside.

“External” in relation to our service is all that is NOT microservice itself, but that communicates directly with it.

In this case, the external is:

  • A system that will notify our service of changes in the quantity of goods
  • A customer who will disable products in manual mode
  • Third-party system DostavchenKO

To emulate requests for the first two, use the MockMvc sprung.
For DostavchenKO emulation use wiremock or MockRestServiceServer.

As a result, our integration test looks like this:

Integration Test
 
 @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 & lt; 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 & lt; 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")));
  }
 }  

What just happened?


We wrote an integration test, the passage of which guarantees the operation of the system for the main user of history. And we did it BEFORE we start implementing the service.

One of the advantages of this approach is that during the writing process I had to go to real DostavchenKO and get from there real answer to real request, which we made our stub. It is very good that we attended to this at the very beginning of development, and not after all the code was written. And here it turns out that the format is not the one specified in the TK, or the service is not available at all, or something else.
I would also like to note that we have not only not yet written any lines of code, which then goes into the prod, but have not even made any any assumptions about how our microservice will be arranged inside: what kind of layers there will be, whether we will use the base, if so, which one, etc. At the time of writing the test, we are abstracted from implementation, and, as we will see later, this can give a number of architectural advantages .

Unlike canonical TDD, where implementation is immediately written after the test, the integration test will not take a very long time. In fact, it will not turn green until the very end of development, until absolutely everything is written, including the perperty files.
We are going further.

Controller


After we wrote the integration test and now we are sure that after we have completed the task, we can sleep at night, it's time to start programming the layers. And the first layer that we will implement is the controller. Why precisely he? Because it is the entry point to the program. We need to move from top to bottom, from the very first layer with which the user will interact, to the last.
This is important.

And again it all starts with the same question:

What do I want from the controller?

Answer:

We know that the controller is engaged in communication with the user, validation and conversion of input data and does not contain business logic. Thus, the answer to this question may be roughly as follows:

I want to:

  • The user was returned BAD_REQUEST when trying to disconnect a product with an invalid id
  • BAD_REQUEST when trying to notify about a product change with an invalid id
  • BAD_REQUEST when trying to report negative quantities
  • INTERNAL_SERVER_ERROR if DostavchenKO is unavailable
  • INTERNAL_SERVER_ERROR, if you could not send to DATA

Since we want to be a user friend, for all the points above, in addition to the http-code, it is necessary to display a custom message describing the problem so that the user understands what the problem is.

  • 200 if the processing was successful
  • INTERNAL_SERVER_ERROR with the default message in all other cases, so as not to shine the light

Until I started writing on TDD, I was the last to think about what my system would bring to the user in some particular and, at first glance, unlikely case. I didn’t think for one simple reason - it’s hard to write an implementation, so it’s not enough RAM to take into account absolutely all regional cases.And after the written implementation, it is still a pleasure to analyze the code that you may not have considered beforehand: we all think that we are writing the perfect code right away). While there is no implementation, there is no need to think about it, and there is no pain to change it, if that. Having written the test first, you do not need to wait until the stars converge, and after the output in the prod, a certain number of systems will fail, and the customer will come running to you with a request to correct something. And this applies not only to the controller.

Start writing tests


With the first three, everything is clear: we use sprint validation, if an invalid requester has arrived, the application will throw out an exception that we will catch in the exception handler. Here, as they say, everything works by itself, but how does the controller know that some third-party system is not available?

It is absolutely clear that the controller itself should know nothing about third-party systems, since what system to ask and what is business logic about, i.e. there must be some kind of intermediary. This mediator is a service. And we will write tests on the controller, using the mock of this service, emulating its behavior in certain cases. So, the service must somehow inform the controller that the system is not available. You can do this in different ways, but the easiest way is to throw a custom escape. We will write a test for this controller behavior.

Test for communication error with third-party DATA system
 
 @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 the Data system \ "\ n" +
  "} \ n" +
  "] \ n" +
  "}")
  );

  }
 }
  


At this stage, a few things appeared by themselves:

  • The service that will be injected into the controller and which will be delegated to process the incoming message according to the new quantity of goods.
  • The method of this service, and accordingly its signature, which will carry out this processing.
  • The realization that the method should throw out a custom action when the system is unavailable.
  • This custom event itself.

Why themselves? Because, as you remember, we have not yet written the implementation. And all these entities appeared in the process of how we program tests. So that the compiler does not swear, in real code, we will have to create everything described above. Fortunately, almost any IDE will help us generate the necessary entities. So we kind of write a test - and the application is filled with classes and methods.

So, the tests on the controller look like this:

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 the 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" +
  "}";
  }
 }  

  Now we can write the implementation and ensure that all tests pass successfully:
Implementation
 
 @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));
  }

 }
  


Exception Handler
 
 @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 - & gt; 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 & lt; ErrorResponse. Message & gt;  fieldErrors = exception.getBindingResult (). getFieldErrors (). stream ()
  .map (fieldError - & gt; 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 ())));
  }
 }  


What just happened?


In TDD, you don't have to keep all the code in your head.

Let's do it again: do not keep all the architecture in memory. Just look at one layer. He is simple.

In the normal process of the brain is not enough, because there are a bunch of implementations. If you are a superhero who knows how to take into account all the nuances of a large project in your head, then TDD is not necessary. I can not do that. The larger the project, the more I am mistaken.

After realizing that you only need to understand what the next layer needs, enlightenment in life comes.The fact is that this approach allows us not to engage in unnecessary things. Here you communicate with the girl. She tells something about a problem at work. And you think how to solve it, you break your head. And she does not need to solve it, she just needs to tell. And that's all. She just wanted to share something. Learning about this at the very first stage listen () is priceless. For everything else ... well, you know.


Service


Then we implement the service.

What do we want from the service?

We want him to do business logic, i.e.:

  1. He knew how to turn off products, and also informed about :
  2. Availability, if the product is not disabled, is available, the color of the goods is yellow, and DostavchenKO is ready to make delivery.
  3. Not available if the product is not available regardless of anything.
  4. Not available if the item is blue.
  5. Inaccessibility if DostavchenKO refuses to carry it.
  6. Not available if the product is manually disabled.
  7. Next, we want the service to throw out an event if any of the systems is unavailable.
  8. And also, in order not to spam DATA, you need to organize a lazy sending of messages, namely:
  9. If we previously sent for the product is available and now calculated what is available, then send nothing.
  10. And if previously unavailable, and now available - send.
  11. And you also need to write it somewhere ...

STOP!


Do not you think that our service starts to do too much?

Judging by our Wishlist, he knows how to turn off goods, and considers accessibility, and makes sure not to send previously sent messages. This is not a high cohesion. It is necessary to make heterogeneous functionalities in different classes, and therefore to be as many as three services: one will deal with the disconnection of goods, the other - calculate the possibility of delivery and transfer it further to the service, which will decide whether to send it or not. By the way, in this way the business logic service will not know anything about the DATA system, which is also a definite plus.

In my experience, quite often, headlong into implementation, it is easy to lose sight of architectural moments. If we wrote the service immediately, without thinking about what he should do, and, more importantly than he shouldn’t, then the probability of overlapping areas of responsibility would increase. From myself I would like to add that it was this example that happened to me in actual practice and the qualitative difference in the results of the TDD and sequential programming approaches inspired me to write this post.

Business Logic


Reflecting on the business logic service for the same reasons of high cohesion, we understand that another level of abstraction is needed between it and the real DostavchenKO. And, since we design the service first , we can demand from the client DostavchenKO the kind of internal contract we want. In the process of writing a test for business logic, we will understand what we want from the client of the following signature:

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

At the service level, we do not care how the real DostavchenKO answers: in the future, the client’s task will somehow get this information out of it. Sometime it may be easy, but sometime it will be necessary to make several requests: at the moment we are abstracted from it.

We want a similar signature from the service that will deal with disabled goods:

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

So, the questions “What do I want from the business logic service?” Written in the tests look like this:

Service Tests
 
 @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);
  }

 }
  


At this stage, by themselves were born:

  • DostavchenKO client with a singularity convenient for service
  • A service where you will need to implement the logic of lazy sending, to whom the designed service will transmit the results of its work
  • Service of disabled products and its signature

Implementation:

Implementation
 
 @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 () & lt; = 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);
  }

 }
  


Disable Products


The time has come for one of the inevitable phases for TDD - refactoring. If you remember, after the implementation of the controller, the service contract looked like this:

  public void disableProduct (long productId)  

And now we decided to bring the disconnect logic to a separate service.

From this service at this stage we want the following:

  • Ability to turn off products.
  • We want it to return that the product is disabled if it has been disabled earlier.
  • We want it to return that the product is available if there hasn’t been a disconnection before.

Looking at the Wishlist, which is a direct consequence of the contract between the business logic service and the projected one, I would like to note the following:

  1. First of all, it’s immediately obvious that the application may have problems if someone wants to turn the disconnected product back, because at the moment this service simply cannot do this. This means that it may be worth discussing this issue with the analyst, who set the task for development. I understand that in this case this question should have arisen immediately after the first reading of the TK, but we are designing a fairly simple system, in larger projects this might not be so obvious. Moreover, we did not know that we would have an entity responsible only for the functionality of disconnecting goods: let me remind you that it was born only during the development process.
  2. Secondly, the service method signature contains only the product identifier. And we will save only the identifier in the collection of disabled goods - at least because we simply have nothing more to enter. Looking ahead, I can say that when we design a service for lazy sending, we will also have to save what we are given for lack of the best, that is, ProductAvailability. As can be seen from the above, we never save the product itself. That is, instead of having a god object, the goods with flags are disabled, available for delivery, and God knows what, as we might have if we didn’t use TDD, we have our own collection of entities in each service. that only does one job.And it turned out, as they say, “self” - we just asked one question: “What do I want from ...” And this is the second example of how, using TDD, we get a more correct architecture.

Tests and implementation are quite simple:

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

 }  


Implementation
 
 @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));
  }

 }
  


Service lazy dispatch


So, we got to the last service, which will ensure that the DATA system is not spammed with the same messages.

Let me remind you that the result of the work of the business logic service, that is, the ProductAvailability object, in which there are only two fields: productId and isAvailable, is already passed to it.

According to the good old tradition, we start thinking about what we want from this service:

  • Sending a notification for the first time anyway.
  • Sending a notification if the availability of a product has changed.
  • Do not send anything if not.
  • If sending to a third-party system has ended with an exception, then the notification that caused the exception should not be sent to the database of sent notifications.
  • Also, in the event of an exception from the DATA side, the service needs to throw out its DataCommunicationException.

Everything is relatively simple here, but I would like to note one thing:

We need information about what we sent before, which means we will have a repository, in which we will save past calculations on the availability of goods.

The ProductAvailability object for saving is not suitable, since at least there is no identifier, which means it is logical to create another one. The main thing here is not to get crazy and not to add this identifier along with @Document (we will use MongoDb as the base) and indexes in ProductAvailability itself.

It should be understood that the ProductAvailability object with all the few fields was created at the design stage of classes that are higher in the call hierarchy than the one we are designing now. These classes do not need to know anything about database-specific fields, since this information was not required when designing.

But this is all talk.

Interestingly, due to the fact that we have already written a bunch of tests with the ProductAvailability that we are transmitting to the service now, adding new fields to it will mean that these tests will also need to be refactored, which may require some effort. And this means that the god object will be much less willing to make a ProductAvailability object than if they wrote the implementation right away: there, on the contrary, it would be easier to add a field to an existing object than to create another class.

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


Implementation
  @ 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 ());
  }

 }  


Conclusion


< br/> A similar application should have been written in practice. And it turned out that at first it was written without TDD, then the business said that it was not needed, and after six months the requirements changed, and it was decided to rewrite it from scratch (good microservice architecture, and it was not so bad to throw out something) .

By writing the same application using different techniques, I can evaluate their differences. In my practice, I saw how TDD helps to build architecture, as it seems to me, more correctly.

I can assume that the reason for this is not the creation of tests itself before implementation, but the fact that, having written the tests at the beginning, we first think about what the created class will do. Also, as long as there is no implementation, we can actually “order” in the called objects exactly the contract that is necessary for the object that causes them, without the temptation to add something quickly somewhere and get an entity that will deal with many tasks at the same time.

In addition to this, one of the main advantages of TDD for me is that I can highlight the fact that I really became more confident in the product that I produce. This may be due to the fact that the average code written on TDD is probably still better covered by tests, but it was after I started writing on TDD that I reduced the number of edits to the code after I gave his testing is almost to zero.

And in general, there was a feeling that as a developer I became better.

The application code can be found here . For those who want to figure out how it was created step by step, I recommend paying attention to the history of commits, after analyzing which, I hope, the process of creating a typical application for TDD will be more understandable.

Here is a very useful video , which I highly recommend for viewing to anyone who wants to plunge into the world of TDD.

The application code repeatedly uses a formatted string like json. This is necessary to check how the application will parse json into POJO objects. If you use IDEA, then quickly and without pain the necessary formatting can be achieved using JSON injections.

What are the cons of the approach?


It is long in development. Programming in the standard paradigm, my colleague could afford to lay out the service for testing by testers without any tests at all, adding them along the way. It was very fast. According to TDD this will not work. If you have tight deadlines, then your managers will be unhappy. Here the trade off between doing well right away, but for a long time and not very well, but quickly. I choose the first for myself, since the second is the result as a result longer. And with more nerves.

According to my feelings, TDD is not suitable if you need to make a big refactoring: because unlike the application created from scratch, it’s not obvious from which side to approach and what to start doing first. It may be that you are working on a class test that you delete as a result.

TDD is not a silver bullet. This is a story about understandable readable code, which can create performance problems. For example, you have created N classes, which, like Fowler, are each engaged in their own business. And then it turns out that in order to do their job, they need everyone to go to the base. And you will have N requests to the database. Instead of, for example, 1 god object and go 1 time. If you are fighting for milliseconds, then using TDD you need to take this into account: readable code is not always the fastest.

And, finally, it is quite difficult to switch to this methodology — you need to teach yourself to think differently. Most pain - in the first stage. I wrote the first integration test for 1.5 days.

Well, the last. If you are using TDD and your code is still not very , then the matter may not be in the methodology. But it helped me.

Source text: How to save on a psychotherapist using test-driven development