A new test method for Spring Boot

After getting used to unit testing, if some codes are not tested before submission, they always feel empty and have no confidence.

The official annotations provided by Spring Boot combined with the powerful Mockito can solve most of the testing requirements. However, it seems that the aspect in agent mode is not satisfactory.

Scenario simulation

Suppose we currently have a studentcontroller in which a getNameById method is stored.

@RestController
public class StudentController {

  @GetMapping("{id}")
  public Student getNameById(@PathVariable Long id) {
    return new Student("Test name");
  }
  public static class Student {
    private String name;

    public Student(String name) {
      this.name = name;
    }

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }
  }
}

Before there is no section, we will access this method to get the corresponding student information with the test name.

Create section

Now, we use the faceted method to append a Yz suffix to the background of the returned name.

@Aspect
@Component
public class AddYzAspect {
  @AfterReturning(value = "execution(* club.yunzhi.smartcommunity.controller.StudentController.getNameById(..))",
      returning = "student")
  public void afterReturnName(StudentController.Student student) {
    student.setName(student.getName() + "Yz");
  }
}

test

If we directly assert the returned name by using the method of ordinary test, it is certainly feasible:

@SpringBootTest
class AddYzAspectTest {
  @Autowired
  StudentController studentController;
  @Test
  void afterReturnName() {
    Assertions.assertEquals(studentController.getNameById(123L).getName(), "Test name Yz");
  }
}

However, the logic in the aspect is often not so simple. In fact, in the actual test, we don't need to care about what happened in the aspect (what happened should be completed in the method of testing the aspect). Here, we are mainly concerned about whether the aspect has been successfully executed, and establish corresponding assertions to prevent inadvertently invalidating the current aspect during future code iterations.

MockBean

Spring Boot provides us with mockbeans to directly Mock a Bean. When testing whether the facet is successfully executed, we do not care about the execution logic of the getNameById() method in the StudentController, so it is applicable to be declared by an appropriate MockBean.

 @SpringBootTest
 class AddYzAspectTest {
-  @Autowired
+  @MockBean
   StudentController studentController;

However, MockBean is not suitable for testing facets. This is because MockBean will directly ignore the annotations of relevant facets when generating new agents, resulting in the direct invalidation of facets.

Meanwhile, although MockBean can be used to simulate Controller, an error will occur if it is used to simulate Aspect.

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration': BeanPostProcessor before instantiation of bean failed; 

MockSpy

In addition to MockBean, Spring Boot is also ready to carry a real Bean, but the Bean can be mocked off at any time as required. At the same time, the Bean generated with this annotation will not destroy the original section.

class AddYzAspectTest {
  @SpyBean
  StudentController studentController;

  @SpyBean
  AddYzAspect addYzAspect;

However, it should be noted that although @ SpyBean successfully generates two beans that can be dropped by Mock, its corresponding slice method will be called automatically once when the corresponding Mock method is executed. For example, the following code will automatically call the afterReturnName method in AddYzAspect.

  @Test
  void afterReturnName() {
    StudentController.Student student = new StudentController.Student("test");
    Mockito.doReturn(student).when(this.studentController).getNameById(123L); 👈 
  }

At this time, because the method dropped by Mock declares the return value, Mockito will use null as the return value to access the afterReturnName method in AddYzAspect. Therefore, a NullPointerException exception will occur:

java.lang.NullPointerException
    at club.yunzhi.smartcommunity.aspects.AddYzAspect.afterReturnName(AddYzAspect.java:14)

Therefore, we need to Mock the relevant methods of the cut plane in advance before mocking the cut method. At the same time, since null will be used as the return value of the method when mocking the cut method, we can write null directly on the corresponding parameters:

  @Test
  void afterReturnName() {
    Mockito.doNothing().when(this.addYzAspect).afterReturnName(null);
    Mockito.doReturn(null).when(this.studentController).getNameById(123L);

Complete test code

@SpringBootTest
class AddYzAspectTest {
  @SpyBean
  StudentController studentController;

  @SpyBean
  AddYzAspect addYzAspect;

  @Test
  void afterReturnName() {
    Mockito.doNothing().when(this.addYzAspect).afterReturnName(null);
    Mockito.doReturn(null).when(this.studentController).getNameById(123L);
    Mockito.verify(this.addYzAspect, Mockito.times(1)).afterReturnName(null);
  }
}

Tags: Spring Boot Testing

Posted on Wed, 01 Dec 2021 09:37:43 -0500 by AbeFroman