Spring Boot 單元測試
非逐句翻譯
目錄
- 單元測試
- 使用@WebMvcTest進行測試
- 使用@DataJpa進行持久層測試
- 使用@JsonTest測試序列化
- 使用MockWebServer測試Spring WebClient Rest調用
- 使用@SpringBootTest進行SpringBoot集成測試
什么是單元測試
當一個測試滿足下面任意一點時,測試就不是單元測試(by Michael Feathers in 2005):
- 與數據庫交流
- 與網絡交流
- 與文件系統交流
- 不能與其他單元測試在同一時間運行
- 不得不為運行它而作一些特別的事
如果一個測試做了上面的任何一條,那么它就是一個集成測試。
不要用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");
}
}
這是一個單元測試嗎?首先@SpringBootTest
注解加載了整個應用上下文,而僅僅是為了注入兩個Bean。
另一個問題是我們需要讀取和寫入訂單到數據庫,這也是集成測試的范疇。
Spring Framework文檔對於單元測試的描述
真正的單元測試運行的非常快,因為不需要運行時去裝配基礎設施。強調將真正的單元測試作為開發方法的一部分可以提高你的生產力。
編寫“可單元測試”的Service
Spring Framework文檔對於單元測試的另一描述
依賴注入可以讓你的代碼減少依賴。POJO可以讓你的應用可以通過
new
操作符在JUnit或TestNG上進行測試,不需要任何的Spring和其他容器
考慮如果編寫這樣的Service,它方便進行單元測試嗎!?
@Service
public class BookService {
@Autowired
private BookRepository repository;
// ... service methods
}
不方便,因為BookRepository
通過@Autowired
被注入到Service中,並且repository
是一個私有變量,這就限定了外界只能通過Spring或其它依賴注入容器(或反射)設置這個值,那么單元測試如果不想加載整個Spring容器,那么它就無法使用這個Service。
而如果這樣寫,使用構造方法注入,外界也可以通過new
去自行傳遞Repository
,這樣即使沒有Spring,外界也能進行快速的測試。這可能也是Spring不推薦屬性注入的原因。
@Service
public class BookService {
private BookRepository repository;
@Autowired
public BookService(BookRepository repository) {
this.repository = repository;
}
}
編寫單元測試
Mockito介紹
前面的知識表明,單元測試就是對一個系統中的某個最小單元的邏輯正確性的測試,通常是對一個方法來進行測試,因為只測試邏輯正確性,所以這個測試是獨立的,不與任何外界環境相關,比如不需要連接數據庫,不訪問網絡和文件系統,不依賴其他單元測試。但是現實的業務邏輯中往往有很多復雜錯綜的依賴關系,比如你想對Service進行單元測試,那么它要依賴一個數據庫持久層的Repository對象,這時候就難辦了,若創建了一個Repository便連接了數據庫,連接了數據庫便不是一個獨立的單元測試。
Mockito是一個用來在單元測試中快速模擬那些需要與外界環境溝通的對象,以便我們快速的、方便的進行單元測試而不用啟動整個系統。
下面的代碼就是Mockito的一個基礎使用,Mock意為偽造。
// 通過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());
編寫單元測試
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");
}
}
現在我們即使不想連接數據庫,也可以通過mock
來給定一個Repository的其他實現,這樣這個方法可以在毫秒內完成。
也可以使用Mockito
@ExtendWith(MockitoExtension.class)
class OrderServiceTests {
@Mock
private OrderRepository orderRepository;
@Mock
private PaymentRepository paymentRepository;
@InjectMocks
private OrderService orderService;
// ...
}