什麼是單元測試
單元測試(英語:Unit Testing)又稱爲模塊測試, 是針對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工作。程序單元是應用的最小可測試部件。在過程化編程中,一個單元就是單個程序、函數、過程等;對於面向對象編程,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法。
通常來說,程序員每修改一次程序就會進行最少一次單元測試,在編寫程序的過程中前後很可能要進行多次單元測試,以證實程序達到軟件規格書要求的工作目標,沒有程序錯誤;雖然單元測試不是什麼必須的,但也不壞,這牽涉到項目管理的政策決定。
單元測試的優點
優質的單元測試可以保障開發質量和程序的魯棒性。在大多數互聯網企業中開發工程師在研發過程中都會頻繁地執行測試用例,運行失敗的單測能幫助我們快速排查和定位問題 使問題在被帶到線上之前完成修復。正如軟件工程界的一條金科玉律----越早發現的缺陷,其修復成本越低。一流的測試能發現未發生的故障;二流的測試能快速定位故障的發生點;三流的測試則疲於奔命,一直跟在故障後面進行功能迴歸。
JAVA中常用的單元測試工具
JUnit/JUnit5
junit是老牌測試框架了,也是目前引用最廣泛的一個框架。當前已經更新到Junit5,功能更強大。
class StandardTests {
@BeforeAll
static void initAll() {
}
@BeforeEach
void init() {
}
@Test
void succeedingTest() {
}
@Test
void failingTest() {
fail("a failing test");
}
@Test
@Disabled("for demonstration purposes")
void skippedTest() {
// not executed
}
@Test
void abortedTest() {
assumeTrue("abc".contains("Z"));
fail("test should have been aborted");
}
@AfterEach
void tearDown() {
}
@AfterAll
static void tearDownAll() {
}
}
assertj
https://assertj.github.io/doc/
一個功能強悍的斷言工具,支持各種斷言方式
// entry point for all assertThat methods and utility methods (e.g. entry)
import static org.assertj.core.api.Assertions.*;
// basic assertions
assertThat(frodo.getName()).isEqualTo("Frodo");
assertThat(frodo).isNotEqualTo(sauron);
// chaining string specific assertions
assertThat(frodo.getName()).startsWith("Fro")
.endsWith("do")
.isEqualToIgnoringCase("frodo");
// collection specific assertions (there are plenty more)
// in the examples below fellowshipOfTheRing is a List<TolkienCharacter>
assertThat(fellowshipOfTheRing).hasSize(9)
.contains(frodo, sam)
.doesNotContain(sauron);
// as() is used to describe the test and will be shown before the error message
assertThat(frodo.getAge()).as("check %s's age", frodo.getName()).isEqualTo(33);
// Java 8 exception assertion, standard style ...
assertThatThrownBy(() -> { throw new Exception("boom!"); }).hasMessage("boom!");
// ... or BDD style
Throwable thrown = catchThrowable(() -> { throw new Exception("boom!"); });
assertThat(thrown).hasMessageContaining("boom");
// using the 'extracting' feature to check fellowshipOfTheRing character's names (Java 7)
assertThat(fellowshipOfTheRing).extracting("name")
.contains("Boromir", "Gandalf", "Frodo", "Legolas")
// same thing using a Java 8 method reference
assertThat(fellowshipOfTheRing).extracting(TolkienCharacter::getName)
.doesNotContain("Sauron", "Elrond");
// extracting multiple values at once grouped in tuples (Java 7)
assertThat(fellowshipOfTheRing).extracting("name", "age", "race.name")
.contains(tuple("Boromir", 37, "Man"),
tuple("Sam", 38, "Hobbit"),
tuple("Legolas", 1000, "Elf"));
// filtering a collection before asserting in Java 7 ...
assertThat(fellowshipOfTheRing).filteredOn("race", HOBBIT)
.containsOnly(sam, frodo, pippin, merry);
// ... or in Java 8
assertThat(fellowshipOfTheRing).filteredOn(character -> character.getName().contains("o"))
.containsOnly(aragorn, frodo, legolas, boromir);
// combining filtering and extraction (yes we can)
assertThat(fellowshipOfTheRing).filteredOn(character -> character.getName().contains("o"))
.containsOnly(aragorn, frodo, legolas, boromir)
.extracting(character -> character.getRace().getName())
.contains("Hobbit", "Elf", "Man");
// and many more assertions: iterable, stream, array, map, dates (java 7 and 8), path, file, numbers, predicate, optional ...
Mockito
一個單元測試中的Mock工具,可以很靈活的創建對象,配合單元測試。
// You can mock concrete classes and interfaces
TrainSeats seats = mock(TrainSeats.class);
// stubbing appears before the actual execution
when(seats.book(Seat.near(WINDOW).in(FIRST_CLASS))).thenReturn(BOOKED);
// the following prints "BOOKED"
System.out.println(seats.book(Seat.near(WINDOW).in(FIRST_CLASS)));
// the following prints "null" because
// .book(Seat.near(AISLE).in(FIRST_CLASS))) was not stubbed
System.out.println(seats.book(Seat.near(AISLE).in(FIRST_CLASS)));
// the following verification passes because
// .book(Seat.near(WINDOW).in(FIRST_CLASS)) has been invoked
verify(seats).book(Seat.near(WINDOW).in(FIRST_CLASS));
// the following verification fails because
// .book(Seat.in(SECOND_CLASS)) has not been invoked
verify(seats).book(Seat.in(SECOND_CLASS));
其他
對於業務代碼,有時單元測試並不方便,因爲每次啓動成本過高。可以使用適當的單元測試方式,比如可以提供一個測試接口,利用IDE的熱部署功能實現不重啓及時修改代碼。
但是對於非業務性代碼,進行單元測試時非常有必要的,可以更早的發現代碼中的問題,同時也可以檢驗程序的解耦性。
良好的代碼設計在單元測試時會更方便,反之緊耦合的設計會給單元測試帶來很大的困擾。