Puff pastry principle

Dedicated to all the fearless on the way from denial to conviction ...


image



There is a fair opinion among developers that if a programmer does not cover the code with tests, then he simply does not understand why they are needed and how to prepare them. It's hard to disagree with this when you already understand what it is about. But how can this precious understanding be achieved?



It's not meant to be...



It so happens that often the most obvious things do not have a clear description among the tons of useful information on the global network. A sort of regular applicant decides to deal with the urgent question “what are unit tests” and stumbles upon a lot of such examples, which are copied from article to article like a tracing paper:



“We have a method that calculates the sum of numbers”



public Integer sum (Integer a, Integer b) {

return a + b

}



“you can write a test for this method”



Test

public void testGoodOne () {

assertThat (sum (2,2), is (4));

}


This is not a joke, this is a simplified example from a typical article about unit testing technology, where at the beginning and at the end there are general phrases about benefit and necessity, and in the middle it is ...



Seeing this, and rereading it twice for the sake of faith, the applicant exclaims: “What a terrible nonsense ? .. "After all, there are practically no methods in his code that receive everything necessary through arguments, and then give an unambiguous result for them. These are typical utilitarian methods and hardly change. But what about complex procedures, embedded dependencies, methods without returning values? There, this approach is not applicable from the word “at all”.



If at this stage the stubborn applicant does not wave his hand and dives further, he soon discovers that MOCs are used for dependencies, for whose methods some conditional behavior is defined, in fact a stub. Here, the applicant can completely blow his mind if there is no kind and patient middle / senior who is ready and able to explain everything nearby ... Otherwise, the applicant for truth completely loses the meaning of “what unit tests are”, since most of the tested method turns out to be some kind of mock fiction , and what is being tested in this case is not clear. Moreover, it is not clear how to organize this for a large, multi-layered application and why this is needed. Thus, at best, the question is postponed until better times, at worst - it hides in a box of damned things.



The most annoying thing is that the test coverage technology is elementarily simple and accessible to everyone, and its benefits are so obvious that any excuses look naive for knowledgeable people. But to figure it out, a beginner lacks some very small, elementary essence, like the flip of a switch.



Key mission



To begin with, I propose to formulate in a nutshell the key function (mission) of the unit tests and the key gain. There are various picturesque options here, but I propose to consider this one: The



key function of unit tests is to record the expected behavior of the system.



and this one: The



key benefit of unit tests is the ability to "run" all the functionality of the application in a matter of seconds.



I recommend remembering this for interviews and will explain a little. Any functionality implies rules of use and results. These requirements come from the business, through systems analytics, and are implemented in code. But the code is constantly evolving, new requirements and improvements come, which can imperceptibly and unexpectedly change something in the finished functionality. This is where unit tests stand guard, which fix the approved rules according to which the system should work! The tests record a scenario that is important for the business, and if after the next revision the test fails, then something is missing: either the developer or the analyst was mistaken, or the new requirements contradict the existing ones and should be clarified, etc. The most important thing is that the “surprise” did not slip through.



A simple, standard unit test made it possible to detect unexpected and probably undesirable system behavior early on. Meanwhile, the system grows and expands, the likelihood of missing its details also grows, and only unit test scripts are able to remember everything and prevent unnoticed departures in time. It is very convenient and reliable, and the main convenience is speed. The application does not even need to run and wander through hundreds of its fields, forms or buttons, you need to run tests and get either full readiness or a bug in a matter of seconds.



So let's remember: fix the expected behavior in the form of unit test scripts, and instantly "run" the application without launching it. This is the absolute value that unit tests can achieve.



But, damn it, how?



Let's move on to the fun part. Modern applications are actively getting rid of monolithicity. Microservices, modules, "layers" - the basic principles of organizing working code, allowing you to achieve independence, ease of reuse, exchange and transfer to systems, etc. Layering and dependency injection are key in our topic.



Consider the layers of a typical web application: controllers, services, repositories, etc. In addition, layers of utilities, facades, models and DTOs are used. The last two should not contain functionality, i.e. methods other than accessors (getters / setters), so you don't need to cover them with tests. We will consider the rest of the layers as targets for coverage.



As this tasty comparison may not suggest, the application cannot be compared with a puff cake for the reason that these layers are embedded in each other, like dependencies:



  • the controller implements the service / s, which it calls for the result
  • the service injects repositories (DAO) into itself, can inject utility components
  • the facade is designed to combine the work of many services or components, respectively, it implements them


The main idea of ​​testing all this stuff in the whole application: covering each layer independently of the other layers. A reference to independence and other anti-monolithic features. Those. if a repository is embedded in the tested service, this “guest” is mocked as part of testing the service, but is personally tested honestly as part of the repository test. Thus, tests are created for each element of each layer, no one is forgotten - everything is in business.



Puff pastry principle



Let's move on to examples, a simple Java Spring Boot application, the code will be elementary, so the essence is easy to understand and similarly applicable to other modern languages ​​/ frameworks. The application will have a simple task - multiply the number by 3, i.e. triple, but at the same time we will create a multi-layered application with dependency injection and layered coverage from head to toe.



image



The structure contains packages for three layers: controller, service, repo. The structure of the tests is similar.

The application will work like this:



  1. from the front-end, a GET request comes to the controller with the identifier of the number that needs to be tripled.
  2. the controller requests the result from its service dependency
  3. the service requests data from its dependency - repository, multiplies and returns the result to the controller
  4. the controller complements the result and returns to the front-end


Let's start with the controller:



@RestController
@RequiredArgsConstructor
public class SomeController {
   private final SomeService someService; // dependency injection

   static final String RESP_PREFIX = ": ";

   static final String PATH_GET_TRIPLE = "/triple/{numberId}";

   @GetMapping(path = PATH_GET_TRIPLE) // mapping method to GET with url=path
   public ResponseEntity<String> triple(@PathVariable(name = "numberId") int numberId) {
       int res = someService.tripleMethod(numberId);   // dependency call
       String resp = RESP_PREFIX + res;                // own logic
       return ResponseEntity.ok().body(resp);
   }
}
      
      





A typical rest controller has dependency injection someService. The triple method is configured for a GET request to the URL "/ triple / {numberId}", where the number ID is passed in the path variable. The method itself can be divided into two main components:



  • accessing a dependency - requesting data from the outside, or calling a procedure with no result
  • own logic - working with existing data


Consider a service:



@Service
@RequiredArgsConstructor
public class SomeService {
   private final SomeRepository someRepository; // dependency injection

   public int tripleMethod(int numberId) {
       Integer fromDB = someRepository.findOne(numberId);  // dependency call
       int res = fromDB * 3;                               // own logic
       return res;
   }
}
      
      





Here is a similar situation: injecting the someRepository dependency, and the method consists of accessing the dependency and its own logic.



Finally - the repository, for simplicity, done without a database:



@Repository
public class SomeRepository {
   public Integer findOne(Integer id){
       return id;
   }
}
      
      





The findOne conditional method supposedly searches the database for a value by identifier, but simply returns the same integer. This does not affect the essence of our example.



If you run our application, then by the configured url you can see:







Works! Layered! In production ...



Oh yes, tests ...



A little about the essence. Writing tests is also a creative process! Therefore, the excuse “I am a developer, not a tester” is completely inappropriate. A good test, like good functionality, requires ingenuity and beauty. But first of all, it is necessary to determine the basic structure of the test.



The testing class contains methods that test the methods of the target class. The minimum that each testing method should contain is a call to the corresponding method of the target class, conditionally speaking like this:



@Test
    void someMethod_test() {
        // prepare...

        int res = someService.someMethod(); 
        
        // check...
    }
      
      





This challenge can be surrounded by Preparation and Review. Preparing data, including input arguments and describing the behavior of the mocks. Validating the results is usually a comparison with the expected value, remember capturing the expected behavior? In total, a test is a scenario that simulates a situation and records that it passed as expected and returned the expected results.



Using the controller as an example, let's try to depict in detail the basic algorithm for writing a test. First of all, the target method of the controller takes an int numberId parameter, let's add it to our script:



int numberId = 42; // input path variable
      
      





The same numberId is transmitted in transit to the input to the service method, and now it's time to provide the service mock:



@MockBean
private SomeService someService;
      
      





The controller's own method code works with the result received from the service, we simulate this result, as well as a call that returns it:




int serviceRes = numberId*3; // result from mock someService
// prepare someService.tripleMethod behavior
when(someService.tripleMethod(eq(numberId))).thenReturn(serviceRes);

      
      





This entry means: "when someService.tripleMethod is called with an argument equal to numberId, return the value of serviceRes."



In addition, this record captures the fact that this service method should be called, which is an important point. It happens that you need to fix a call to a procedure without a result, then a different notation is used, conventionally such as "do nothing when ..."




Mockito.doNothing().when(someService).someMethod(eq(someParam));

      
      





Again, here is just an imitation of the work of someService, honest testing with detailed fixing of the behavior of someService will be implemented separately. Moreover, it doesn't even matter here that the value should triple, if we write




int serviceRes = numberId*5; 
      
      





this will not break the current script, since it is not the behavior of someService that is captured here, but the behavior of the controller that takes the result of someService for granted. This is completely logical, because the target class cannot be responsible for the behavior of the injected dependency, but has to trust it.



So we have defined the behavior of the mock in our script, therefore, when executing the test, when inside the call to the target method it comes to a mock, it will return what was asked for - serviceRes, and then the controller's own code will work with this value.



Next, we place a call to the target method in the script. The controller method has a peculiarity - it is not called explicitly in the code, but is bound via the HTTP GET method and URL, therefore in tests it is called through a special test client. In Spring, this is MockMvc, in other frameworks there are analogues, for example, WebTestCase.createClient in Symfony. So, further, it is simple to execute the controller method through mapping by GET and URL.




       //// mockMvc.perform
       MockHttpServletRequestBuilder requestConfig = MockMvcRequestBuilders.get(SomeController.PATH_GET_TRIPLE, numberId);

       MvcResult mvcResult = mockMvc.perform(requestConfig)
           .andExpect(status().isOk())
           //.andDo(MockMvcResultHandlers.print())
           .andReturn()
       ;//// mockMvc.perform

      
      





It is also checked there that such a mapping exists at all. If the call is successful, it comes down to checking and fixing the results. For example, you can fix how many times the mock method was called:




// check of calling
Mockito.verify(someService, Mockito.atLeastOnce()).tripleMethod(eq(numberId));

      
      





In our case, this is redundant, since we have already fixed its only call via when, but sometimes this method is appropriate.



And now the main thing - we check the behavior of the controller's own code:




// check of result
assertEquals(SomeController.RESP_PREFIX+serviceRes, mvcResult.getResponse().getContentAsString());

      
      





Here we have fixed what the method itself is responsible for - that the result received from someService is concatenated with the controller prefix, and it is this line that goes into the response body. By the way, you can see with your own eyes the content of Body if you uncomment the line




//.andDo(MockMvcResultHandlers.print())

      
      





but usually this printing to the console is used only as an aid to debugging.



Thus, we have a test method in the controller test class:




@WebMvcTest(SomeController.class)
class SomeControllerTest {
   @MockBean
   private SomeService someService;

   @Autowired
   private MockMvc mockMvc;

   @Test
   void triple() throws Exception {
       int numberId = 42; // input path variable
       int serviceRes = numberId*3; // result from mock someService
       // prepare someService.tripleMethod behavior
       when(someService.tripleMethod(eq(numberId))).thenReturn(serviceRes);

       //// mockMvc.perform
       MockHttpServletRequestBuilder requestConfig = MockMvcRequestBuilders.get(SomeController.PATH_GET_TRIPLE, numberId);

       MvcResult mvcResult = mockMvc.perform(requestConfig)
           .andExpect(status().isOk())
           //.andDo(MockMvcResultHandlers.print())
           .andReturn()
       ;//// mockMvc.perform

       // check of calling
       Mockito.verify(someService, Mockito.atLeastOnce()).tripleMethod(eq(numberId));
       // check of result
       assertEquals(SomeController.RESP_PREFIX+serviceRes, mvcResult.getResponse().getContentAsString());
   }
}
      
      





Now it's time to honestly test the someService.tripleMethod method, where similarly there is a dependency call and your own code. Prepare an arbitrary input argument and simulate the behavior of the someRepository dependency:




int numberId = 42;
when(someRepository.findOne(eq(numberId))).then(AdditionalAnswers.returnsFirstArg());

      
      





Translation: "when someRepository.findOne is called with an argument equal to numberId, return the same argument." A similar situation - here we do not check the logic of dependence, but we take its word for it. We only capture the call to the dependency within this method. The principle here is the service's own logic, its area of ​​responsibility:




assertEquals(numberId*3, res);

      
      





We fix that the value received from the repository should be tripled by the method's own logic. Now this test is guarding this requirement:




@ExtendWith(MockitoExtension.class)
class SomeServiceTest {
   @Mock
   private SomeRepository someRepository; // ,  

   @InjectMocks
   private SomeService someService; //   ,  

   @Test
   void tripleMethod() {
       int numberId = 42;
       when(someRepository.findOne(eq(numberId))).then(AdditionalAnswers.returnsFirstArg());

       int res = someService.tripleMethod(numberId);

       assertEquals(numberId*3, res);
   }
}
      
      





Since our repository is conditionally toy, then the test turned out to be appropriate:




class SomeRepositoryTest {
   // no dependency injection
   private final SomeRepository someRepository = new SomeRepository();

   @Test
   void findOne() {
       int id = 777;
       Integer fromDB = someRepository.findOne(id);
       assertEquals(id, fromDB);
   }
}
      
      





However, even here the whole skeleton is in place: preparation, invocation and verification. Thus, the correct work of someRepository.findOne is fixed.



A real repository requires testing with raising the database in memory or in a test container, migrating the structure and data, sometimes inserting test records. This is often the longest testing layer, but no less important because successful migration, saving models, correct selection, etc. are recorded. Organization of database testing is beyond the scope of this article, but it is precisely described in detail in the manuals. There is no dependency injection in the repository and is not necessary, its task is to work with the database. In our case, it would be a test with preliminary saving of a record to the database and subsequent search by id.



Thus, we have achieved full coverage of the entire functional chain. Each test is responsible for running its own code and captures calls to all dependencies. Testing an application does not require running it with full context raising, which is hard and time-consuming. Maintaining functionality with fast and easy unit tests creates a comfortable and reliable work environment.



In addition, tests improve the quality of the code. As part of independent testing in layers, you often need to rethink how you organize your code. For example, a method has been created in the service first, it is not small, it contains both its own code and mocks, and, for example, it does not make sense to split it, it is covered by the test / s in full - all preparations and checks are defined. Then someone decides to add a second method to the service, which calls the first method. It seems once a common situation, but when it comes to coverage with a test, something does not add up ... For the second method, you will have to describe the second scenario and duplicate the first preparation scenario? After all, it will not work to lock the first method of the tested class itself.



Perhaps, in this case, it is appropriate to think about a different organization of the code. There are two opposite approaches:



  • move the first method into a utility component that is injected as a dependency into the service.
  • move the second method into a service facade that combines different methods of the embedded service or even several services.


Both of these options fit well into the "layers" principle and are conveniently tested with dependency mocking. The beauty is that each layer is responsible for its own work, and together they create a solid framework for the invulnerability of the whole system.



On the track ...



Interview question: How many times should a developer run tests within a ticket? As many as you like, but at least twice:



  • before starting work, to make sure that everything is OK, and not to find out later what has already been broken, and not you
  • at the end of work


So why write tests? Then, that it is not worth trying to remember and foresee everything in a large, complex application, it must be entrusted to automation. A developer who does not own auto-testing is not ready to participate in a large project, any interviewee will immediately reveal this.



Therefore, I recommend developing these skills if you want to qualify for high salaries. You can start this exciting practice with basic things, namely, within the framework of your favorite framework, learn how to test:



  • components with embedded dependencies, mocking techniques
  • controllers, because there are nuances of calling the end point
  • DAO, repositories, including raising the test base and migrations


I hope this concept of "puff pastry" has helped to understand the technique of testing complex applications and to feel how flexible and powerful a tool is presented to us to work with. Of course, the better the tool, the more skillful work it requires.



Enjoy your work and great skill!



The example code is available from the link on github.com: https://github.com/denisorlov/examples/tree/main/unittestidea



All Articles