Please write an elegant unit test for SpringBoot?

What is unit testing

A test is not a unit test when any of the following is true for a test (by Michael Feathers in 2005):

  1. communicate with the database
  2. communicate with the network
  3. Communicating with the file system
  4. Cannot be run at the same time as other unit tests
  5. had to do something special to run it

If a test does any of the above, then it is an integration test.

Don't write unit tests with Spring

@SpringBootTest
class OrderServiceTests {
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private OrderService orderService;

    @Test
    void payOrder() {
        Order order = new Order(1L, false);
        orderRepository.save(order);

        Payment payment = orderService.pay(1L, "4532756279624064");

        assertThat(payment.getOrder().isPaid()).isTrue();
        assertThat(payment.getCreditCardNumber()).isEqualTo("4532756279624064");
    }
}

Is this a unit test? First, @SpringBootTestthe annotation loads the entire application context, just to inject two beans.

Another problem is that we need to read and write orders to the database, which is also the scope of integration testing.

The Spring Framework documentation describes unit testing

True unit tests run very fast because there is no need for the runtime to wire up the infrastructure. Emphasizing true unit testing as part of your development methodology can increase your productivity.

Write a "unit testable" Service

Another description of unit testing in the Spring Framework documentation

Dependency injection can make your code less dependent. POJO allows your application to newbe tested on JUnit or TestNG through operators, without any need for Spring and other containers

Consider if such a Service is written, is it convenient for unit testing! ?

@Service
public class BookService {
    @Autowired
    private BookRepository repository;

    // ... service methods

}

Inconvenient, because BookRepositoryit @Autowiredis injected into the Service and repositoryis a private variable, which limits the outside world to only set this value through Spring or other dependency injection containers (or reflection). If the unit test does not want to load the entire Spring container, then It cannot use this Service.

And if it is written like this, using constructor injection, the outside world can also newpass it by itself Repository, so that even without Spring, the outside world can perform quick tests. This may also be why Spring does not recommend attribute injection.

@Service
public class BookService {
    private BookRepository repository;

    @Autowired
    public BookService(BookRepository repository) {
        this.repository = repository;
    }
}

Write unit tests

Introduction to Mockito

The previous knowledge shows that a unit test is a test of the logical correctness of a certain smallest unit in a system, usually a method is tested, because only the logical correctness is tested, so this test is independent and not related to any It is related to the external environment, such as no need to connect to the database, no access to the network and file system, and no dependence on other unit tests. However, there are often many complex and intricate dependencies in real business logic. For example, if you want to unit test a Service, it depends on a Repository object of the database persistence layer. This is difficult. If you create a Repository, you can connect Connecting to a database is not a stand-alone unit test.

Mockito is used to quickly simulate objects that need to communicate with the external environment in unit tests, so that we can quickly and conveniently conduct unit tests without starting the entire system.

The following code is a basic use of Mockito, Mock means fake.

// 通过mock方法伪造一个orderRepository的实现,这个实现目前什么都不会做
orderRepository = mock(OrderRepository.class);
// 通过mock方法伪造一个paymentRepository的实现,这个实现目前什么都不会做
paymentRepository = mock(PaymentRepository.class)

// 创建一个Order对象以便一会儿使用
Order order = new Order(1L, false);
// 使用when方法,定义当orderRepository.findById(1L)被调用时的行为,直接返回刚刚创建的order对象
when(orderRepository.findById(1L)).thenReturn(Optional.of(order));
// 使用when方法,定义当paymentRepository.save(任何参数)被调用时的行为,直接返回传入的参数。
when(paymentRepository.save(any())).then(returnsFirstArg());

Write unit tests

class OrderServiceTests {
    private OrderRepository orderRepository;
    private PaymentRepository paymentRepository;
    private OrderService orderService;

    @BeforeEach
    void setupService() {
        orderRepository = mock(OrderRepository.class);
        paymentRepository = mock(PaymentRepository.class);
        orderService = new OrderService(orderRepository, paymentRepository);
    }
    
    @Test
    void payOrder() {
        Order order = new Order(1L, false);
        when(orderRepository.findById(1L)).thenReturn(Optional.of(order));
        when(paymentRepository.save(any())).then(returnsFirstArg());

        Payment payment = orderService.pay(1L, "4532756279624064");

        assertThat(payment.getOrder().isPaid()).isTrue();
        assertThat(payment.getCreditCardNumber()).isEqualTo("4532756279624064");
    }
}

Now even if we don't want to connect to the database, we can also mockgive an other implementation of Repository, so that this method can be completed in milliseconds.

can also useMockito

@ExtendWith(MockitoExtension.class)
class OrderServiceTests {
    @Mock
    private OrderRepository orderRepository;
    @Mock
    private PaymentRepository paymentRepository;
    @InjectMocks
    private OrderService orderService;
    
    // ...
}

Guess you like

Origin blog.csdn.net/agonie201218/article/details/131766260