1. 程式人生 > 其它 >SpringBoot測試迷你係列 —— 一 單元測試

SpringBoot測試迷你係列 —— 一 單元測試

Spring Boot 單元測試

原文Spring Boot Unit Testing

非逐句翻譯

目錄

  1. 單元測試
  2. 使用@WebMvcTest進行測試
  3. 使用@DataJpa進行持久層測試
  4. 使用@JsonTest測試序列化
  5. 使用MockWebServer測試Spring WebClient Rest呼叫
  6. 使用@SpringBootTest進行SpringBoot整合測試

什麼是單元測試

當一個測試滿足下面任意一點時,測試就不是單元測試(by Michael Feathers in 2005):

  1. 與資料庫交流
  2. 與網路交流
  3. 與檔案系統交流
  4. 不能與其他單元測試在同一時間執行
  5. 不得不為執行它而作一些特別的事

如果一個測試做了上面的任何一條,那麼它就是一個整合測試。

不要用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;
    
    // ...
}