Test-driven development
主要是通過不斷循環以下兩個小步驟将需求一步步實作:
- 在寫新代碼之前先寫好執行失敗的測試用例
- 再寫最少的代碼實作使用前一步的新用例執行成功
實際上是這種開發循環[test, code, refactor, (repeat)
TDD 三大優點:
- 實作的代碼都是為了實作需求邏輯的
- 寫出的代碼都是經過測試的,沒有BUG的
- 測試方法可以作為文檔,通過測試代碼很容易了解代碼解決的問題
通過下面的需求實作過程說明TDD的開發過程
航班添加乘客業務邏輯:經濟航班可以加任何類型乘客,商業航班隻加VIP乘客
移除乘客邏輯:可以任意移除普通乘客,不允許移除VIP乘客
第一版設計
Fight類
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Flight {
private String id;
private List<Passenger> passengers = new ArrayList<Passenger>();
private String flightType;
public Flight(String id, String flightType) {
this.id = id;
this.flightType = flightType;
}
public String getId() {
return id;
}
public List<Passenger> getPassengersList() {
return Collections.unmodifiableList(passengers);
}
public String getFlightType() {
return flightType;
}
public boolean addPassenger(Passenger passenger) {
switch (flightType) {
case "Economy":
return passengers.add(passenger);
case "Business":
if (passenger.isVip()) {
return passengers.add(passenger);
}
return false;
default:
throw new RuntimeException("Unknown type: " + flightType);
}
}
public boolean removePassenger(Passenger passenger) {
switch (flightType) {
case "Economy":
if (!passenger.isVip()) {
return passengers.remove(passenger);
}
return false;
case "Business":
return false;
default:
throw new RuntimeException("Unknown type: " + flightType);
}
}
}
Passenger類
public class Passenger {
private String name;
private boolean vip;
public Passenger(String name, boolean vip) {
this.name = name;
this.vip = vip;
}
public String getName() {
return name;
}
public boolean isVip() {
return vip;
}
}
通過Airport的main方法測試
public class Airport {
public static void main(String[] args) {
Flight economyFlight = new Flight("1", "Economy");
Flight businessFlight = new Flight("2", "Business");
Passenger james = new Passenger("James", true);
Passenger mike = new Passenger("Mike", false);
businessFlight.addPassenger(james);
businessFlight.removePassenger(james);
businessFlight.addPassenger(mike);
economyFlight.addPassenger(mike);
System.out.println("Business flight passengers list:");
for (Passenger passenger : businessFlight.getPassengersList()) {
System.out.println(passenger.getName());
}
System.out.println("Economy flight passengers list:");
for (Passenger passenger : economyFlight.getPassengersList()) {
System.out.println(passenger.getName());
}
}
}
改成TDD方式測試
引入Junit5
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.6.0</version>
<scope>test</scope>
</dependency>
</dependencies>
AirportTest
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class AirportTest {
@DisplayName("Given there is an economy flight")
@Nested
class EconomyFlightTest {
private Flight economyFlight;
@BeforeEach
void setUp() {
economyFlight = new Flight("1", "Economy");
}
@Test
public void testEconomyFlightRegularPassenger() {
Passenger mike = new Passenger("Mike", false);
assertEquals("1", economyFlight.getId());
assertEquals(true, economyFlight.addPassenger(mike));
assertEquals(1, economyFlight.getPassengersList().size());
assertEquals("Mike", economyFlight.getPassengersList().get(0).getName());
assertEquals(true, economyFlight.removePassenger(mike));
assertEquals(0, economyFlight.getPassengersList().size());
}
@Test
public void testEconomyFlightVipPassenger() {
Passenger james = new Passenger("James", true);
assertEquals("1", economyFlight.getId());
assertEquals(true, economyFlight.addPassenger(james));
assertEquals(1, economyFlight.getPassengersList().size());
assertEquals("James", economyFlight.getPassengersList().get(0).getName());
assertEquals(false, economyFlight.removePassenger(james));
assertEquals(1, economyFlight.getPassengersList().size());
}
}
@DisplayName("Given there is a business flight")
@Nested
class BusinessFlightTest {
private Flight businessFlight;
@BeforeEach
void setUp() {
businessFlight = new Flight("2", "Business");
}
@Test
public void testBusinessFlightRegularPassenger() {
Passenger mike = new Passenger("Mike", false);
assertEquals(false, businessFlight.addPassenger(mike));
assertEquals(0, businessFlight.getPassengersList().size());
assertEquals(false, businessFlight.removePassenger(mike));
assertEquals(0, businessFlight.getPassengersList().size());
}
@Test
public void testBusinessFlightVipPassenger() {
Passenger james = new Passenger("James", true);
assertEquals(true, businessFlight.addPassenger(james));
assertEquals(1, businessFlight.getPassengersList().size());
assertEquals(false, businessFlight.removePassenger(james));
assertEquals(1, businessFlight.getPassengersList().size());
}
}
}
執行測試用例後發現Airport類沒使用到,可以去掉。
Fight覆寫率小于100%,發現getFlightType沒使用到,switch塊的default case沒覆寫,考慮重構移除未使用的代碼。
開始着手重構事宜:
可以通過多态代替switch條件,利用多态特性(運作時而非編譯時才确定實際調用具體的方法),使得開發的代碼符合**開閉原則,**避免每次增加航班類型都要修改存在的類。
第二版設計
Flight
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public abstract class Flight {
private String id;
List<Passenger> passengers = new ArrayList<Passenger>();
public Flight(String id) {
this.id = id;
}
public String getId() {
return id;
}
public List<Passenger> getPassengers() {
return Collections.unmodifiableList(passengers);
}
public abstract boolean addPassenger(Passenger passenger);
public abstract boolean removePassenger(Passenger passenger);
}
EconomyFlight
public class EconomyFlight extends Flight {
public EconomyFlight(String id) {
super(id);
}
@Override
public boolean addPassenger(Passenger passenger) {
return passengers.add(passenger);
}
@Override
public boolean removePassenger(Passenger passenger) {
if (!passenger.isVip()) {
return passengers.remove(passenger);
}
return false;
}
}
BusinessFlight
public class BusinessFlight extends Flight {
public BusinessFlight(String id) {
super(id);
}
@Override
public boolean addPassenger(Passenger passenger) {
if (passenger.isVip()) {
return passengers.add(passenger);
}
return false;
}
@Override
public boolean removePassenger(Passenger passenger) {
return false;
}
}
AirportTest
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class AirportTest {
@DisplayName("Given there is an economy flight")
@Nested
class EconomyFlightTest {
private Flight economyFlight;
@BeforeEach
void setUp() {
economyFlight = new EconomyFlight("1");
}
@Test
public void testEconomyFlightRegularPassenger() {
Passenger mike = new Passenger("Mike", false);
assertEquals("1", economyFlight.getId());
assertEquals(true, economyFlight.addPassenger(mike));
assertEquals(1, economyFlight.getPassengers().size());
assertEquals("Mike", economyFlight.getPassengers().get(0).getName());
assertEquals(true, economyFlight.removePassenger(mike));
assertEquals(0, economyFlight.getPassengers().size());
}
@Test
public void testEconomyFlightVipPassenger() {
Passenger james = new Passenger("James", true);
assertEquals("1", economyFlight.getId());
assertEquals(true, economyFlight.addPassenger(james));
assertEquals(1, economyFlight.getPassengers().size());
assertEquals("James", economyFlight.getPassengers().get(0).getName());
assertEquals(false, economyFlight.removePassenger(james));
assertEquals(1, economyFlight.getPassengers().size());
}
}
@DisplayName("Given there is a business flight")
@Nested
class BusinessFlightTest {
private Flight businessFlight;
@BeforeEach
void setUp() {
businessFlight = new BusinessFlight("2");
}
@Test
public void testBusinessFlightRegularPassenger() {
Passenger mike = new Passenger("Mike", false);
assertEquals(false, businessFlight.addPassenger(mike));
assertEquals(0, businessFlight.getPassengers().size());
assertEquals(false, businessFlight.removePassenger(mike));
assertEquals(0, businessFlight.getPassengers().size());
}
@Test
public void testBusinessFlightVipPassenger() {
Passenger james = new Passenger("James", true);
assertEquals(true, businessFlight.addPassenger(james));
assertEquals(1, businessFlight.getPassengers().size());
assertEquals(false, businessFlight.removePassenger(james));
assertEquals(1, businessFlight.getPassengers().size());
}
}
}
來了新需求,增加一種新類型航班:隻允許VIP乘客乘坐,其他類型乘客不允許乘坐;可以移除任何類型的乘客。
先實作簡單的PremiumFlight
public class PremiumFlight extends Flight {
public PremiumFlight(String id) {
super(id);
}
@Override
public boolean addPassenger(Passenger passenger) {
return false;
}
@Override
public boolean removePassenger(Passenger passenger) {
return false;
}
}
AirportTest
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class AirportTest {
@DisplayName("Given there is an economy flight")
@Nested
class EconomyFlightTest {
private Flight economyFlight;
private Passenger mike;
private Passenger james;
@BeforeEach
void setUp() {
economyFlight = new EconomyFlight("1");
mike = new Passenger("Mike", false);
james = new Passenger("James", true);
}
@Nested
@DisplayName("When we have a regular passenger")
class RegularPassenger {
@Test
@DisplayName("Then you can add and remove him from an economy flight")
public void testEconomyFlightRegularPassenger() {
assertAll("Verify all conditions for a regular passenger and an economy flight",
() -> assertEquals("1", economyFlight.getId()),
() -> assertEquals(true, economyFlight.addPassenger(mike)),
() -> assertEquals(1, economyFlight.getPassengersList().size()),
() -> assertEquals("Mike", economyFlight.getPassengersList().get(0).getName()),
() -> assertEquals(true, economyFlight.removePassenger(mike)),
() -> assertEquals(0, economyFlight.getPassengersList().size())
);
}
}
@Nested
@DisplayName("When we have a VIP passenger")
class VipPassenger {
@Test
@DisplayName("Then you can add him but cannot remove him from an economy flight")
public void testEconomyFlightVipPassenger() {
assertAll("Verify all conditions for a VIP passenger and an economy flight",
() -> assertEquals("1", economyFlight.getId()),
() -> assertEquals(true, economyFlight.addPassenger(james)),
() -> assertEquals(1, economyFlight.getPassengersList().size()),
() -> assertEquals("James", economyFlight.getPassengersList().get(0).getName()),
() -> assertEquals(false, economyFlight.removePassenger(james)),
() -> assertEquals(1, economyFlight.getPassengersList().size())
);
}
}
}
@DisplayName("Given there is a business flight")
@Nested
class BusinessFlightTest {
private Flight businessFlight;
private Passenger mike;
private Passenger james;
@BeforeEach
void setUp() {
businessFlight = new BusinessFlight("2");
mike = new Passenger("Mike", false);
james = new Passenger("James", true);
}
@Nested
@DisplayName("When we have a regular passenger")
class RegularPassenger {
@Test
@DisplayName("Then you cannot add or remove him from a business flight")
public void testBusinessFlightRegularPassenger() {
assertAll("Verify all conditions for a regular passenger and a business flight",
() -> assertEquals(false, businessFlight.addPassenger(mike)),
() -> assertEquals(0, businessFlight.getPassengersList().size()),
() -> assertEquals(false, businessFlight.removePassenger(mike)),
() -> assertEquals(0, businessFlight.getPassengersList().size())
);
}
}
@Nested
@DisplayName("When we have a VIP passenger")
class VipPassenger {
@Test
@DisplayName("Then you can add him but cannot remove him from a business flight")
public void testBusinessFlightVipPassenger() {
assertAll("Verify all conditions for a VIP passenger and a business flight",
() -> assertEquals(true, businessFlight.addPassenger(james)),
() -> assertEquals(1, businessFlight.getPassengersList().size()),
() -> assertEquals(false, businessFlight.removePassenger(james)),
() -> assertEquals(1, businessFlight.getPassengersList().size())
);
}
}
}
@DisplayName("Given there is a premium flight")
@Nested
class PremiumFlightTest {
private Flight premiumFlight;
private Passenger mike;
private Passenger james;
@BeforeEach
void setUp() {
premiumFlight = new PremiumFlight("3");
mike = new Passenger("Mike", false);
james = new Passenger("James", true);
}
@Nested
@DisplayName("When we have a regular passenger")
class RegularPassenger {
@Test
@DisplayName("Then you cannot add or remove him from a premium flight")
public void testPremiumFlightRegularPassenger() {
assertAll("Verify all conditions for a regular passenger and a premium flight",
() -> assertEquals(false, premiumFlight.addPassenger(mike)),
() -> assertEquals(0, premiumFlight.getPassengersList().size()),
() -> assertEquals(false, premiumFlight.removePassenger(mike)),
() -> assertEquals(0, premiumFlight.getPassengersList().size())
);
}
}
@Nested
@DisplayName("When we have a VIP passenger")
class VipPassenger {
@Test
@DisplayName("Then you can add and remove him from a premium flight")
public void testPremiumFlightVipPassenger() {
assertAll("Verify all conditions for a VIP passenger and a premium flight",
() -> assertEquals(true, premiumFlight.addPassenger(james)),
() -> assertEquals(1, premiumFlight.getPassengersList().size()),
() -> assertEquals(true, premiumFlight.removePassenger(james)),
() -> assertEquals(0, premiumFlight.getPassengersList().size())
);
}
}
}
}
運作測試用例發現有個測試用例失敗,需要實作邏輯使測試用到通過;另外發現普通類型的乘客的測試用例是通過的,說明簡單的代碼已經滿足業務邏輯,隻需要實作VIP類型乘客的邏輯。
實作VIP乘客的業務邏輯:
public class PremiumFlight extends Flight {
public PremiumFlight(String id) {
super(id);
}
@Override
public boolean addPassenger(Passenger passenger) {
if (passenger.isVip()) {
return passengers.add(passenger);
}
return false;
}
@Override
public boolean removePassenger(Passenger passenger) {
if (passenger.isVip()) {
return passengers.remove(passenger);
}
return false;
}
}
偶然發現程式有個BUG,同一個乘客可以重複添加,這應該不允許的,需要增加這段邏輯。TDD方式先寫測試用例,使用@RepeatedTest及RepetitionInfo實作重複執行。
AirportTest
import org.junit.jupiter.api.*;
import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.*;
public class AirportTest {
@DisplayName("Given there is an economy flight")
@Nested
class EconomyFlightTest {
private Flight economyFlight;
private Passenger mike;
private Passenger james;
@BeforeEach
void setUp() {
economyFlight = new EconomyFlight("1");
mike = new Passenger("Mike", false);
james = new Passenger("James", true);
}
@Nested
@DisplayName("When we have a regular passenger")
class RegularPassenger {
@Test
@DisplayName("Then you can add and remove him from an economy flight")
public void testEconomyFlightRegularPassenger() {
assertAll("Verify all conditions for a regular passenger and an economy flight",
() -> assertEquals("1", economyFlight.getId()),
() -> assertEquals(true, economyFlight.addPassenger(mike)),
() -> assertEquals(1, economyFlight.getPassengersSet().size()),
() -> assertEquals("Mike", new ArrayList<>(economyFlight.getPassengersSet()).get(0).getName()),
() -> assertEquals(true, economyFlight.removePassenger(mike)),
() -> assertEquals(0, economyFlight.getPassengersSet().size())
);
}
@DisplayName("Then you cannot add him to an economy flight more than once")
@RepeatedTest(5)
public void testEconomyFlightRegularPassengerAddedOnlyOnce(RepetitionInfo repetitionInfo) {
for (int i = 0; i < repetitionInfo.getCurrentRepetition(); i++) {
economyFlight.addPassenger(mike);
}
assertAll("Verify a regular passenger can be added to an economy flight only once",
() -> assertEquals(1, economyFlight.getPassengersSet().size()),
() -> assertTrue(economyFlight.getPassengersSet().contains(mike)),
() -> assertTrue(new ArrayList<>(economyFlight.getPassengersSet()).get(0).getName().equals("Mike"))
);
}
}
@Nested
@DisplayName("When we have a VIP passenger")
class VipPassenger {
@Test
@DisplayName("Then you can add him but cannot remove him from an economy flight")
public void testEconomyFlightVipPassenger() {
assertAll("Verify all conditions for a VIP passenger and an economy flight",
() -> assertEquals("1", economyFlight.getId()),
() -> assertEquals(true, economyFlight.addPassenger(james)),
() -> assertEquals(1, economyFlight.getPassengersSet().size()),
() -> assertEquals("James", new ArrayList<>(economyFlight.getPassengersSet()).get(0).getName()),
() -> assertEquals(false, economyFlight.removePassenger(james)),
() -> assertEquals(1, economyFlight.getPassengersSet().size())
);
}
@DisplayName("Then you cannot add him to an economy flight more than once")
@RepeatedTest(5)
public void testEconomyFlightVipPassengerAddedOnlyOnce(RepetitionInfo repetitionInfo) {
for (int i = 0; i < repetitionInfo.getCurrentRepetition(); i++) {
economyFlight.addPassenger(james);
}
assertAll("Verify a VIP passenger can be added to an economy flight only once",
() -> assertEquals(1, economyFlight.getPassengersSet().size()),
() -> assertTrue(economyFlight.getPassengersSet().contains(james)),
() -> assertTrue(new ArrayList<>(economyFlight.getPassengersSet()).get(0).getName().equals("James"))
);
}
}
}
@DisplayName("Given there is a business flight")
@Nested
class BusinessFlightTest {
private Flight businessFlight;
private Passenger mike;
private Passenger james;
@BeforeEach
void setUp() {
businessFlight = new BusinessFlight("2");
mike = new Passenger("Mike", false);
james = new Passenger("James", true);
}
@Nested
@DisplayName("When we have a regular passenger")
class RegularPassenger {
@Test
@DisplayName("Then you cannot add or remove him from a business flight")
public void testBusinessFlightRegularPassenger() {
assertAll("Verify all conditions for a regular passenger and a business flight",
() -> assertEquals(false, businessFlight.addPassenger(mike)),
() -> assertEquals(0, businessFlight.getPassengersSet().size()),
() -> assertEquals(false, businessFlight.removePassenger(mike)),
() -> assertEquals(0, businessFlight.getPassengersSet().size())
);
}
}
@Nested
@DisplayName("When we have a VIP passenger")
class VipPassenger {
@Test
@DisplayName("Then you can add him but cannot remove him from a business flight")
public void testBusinessFlightVipPassenger() {
assertAll("Verify all conditions for a VIP passenger and a business flight",
() -> assertEquals(true, businessFlight.addPassenger(james)),
() -> assertEquals(1, businessFlight.getPassengersSet().size()),
() -> assertEquals(false, businessFlight.removePassenger(james)),
() -> assertEquals(1, businessFlight.getPassengersSet().size())
);
}
@DisplayName("Then you cannot add him to a business flight more than once")
@RepeatedTest(5)
public void testBusinessFlightVipPassengerAddedOnlyOnce(RepetitionInfo repetitionInfo) {
for (int i = 0; i < repetitionInfo.getCurrentRepetition(); i++) {
businessFlight.addPassenger(james);
}
assertAll("Verify a VIP passenger can be added to a business flight only once",
() -> assertEquals(1, businessFlight.getPassengersSet().size()),
() -> assertTrue(businessFlight.getPassengersSet().contains(james)),
() -> assertTrue(new ArrayList<>(businessFlight.getPassengersSet()).get(0).getName().equals("James"))
);
}
}
}
@DisplayName("Given there is a premium flight")
@Nested
class PremiumFlightTest {
private Flight premiumFlight;
private Passenger mike;
private Passenger james;
@BeforeEach
void setUp() {
premiumFlight = new PremiumFlight("3");
mike = new Passenger("Mike", false);
james = new Passenger("James", true);
}
@Nested
@DisplayName("When we have a regular passenger")
class RegularPassenger {
@Test
@DisplayName("Then you cannot add or remove him from a premium flight")
public void testPremiumFlightRegularPassenger() {
assertAll("Verify all conditions for a regular passenger and a premium flight",
() -> assertEquals(false, premiumFlight.addPassenger(mike)),
() -> assertEquals(0, premiumFlight.getPassengersSet().size()),
() -> assertEquals(false, premiumFlight.removePassenger(mike)),
() -> assertEquals(0, premiumFlight.getPassengersSet().size())
);
}
}
@Nested
@DisplayName("When we have a VIP passenger")
class VipPassenger {
@Test
@DisplayName("Then you can add and remove him from a premium flight")
public void testPremiumFlightVipPassenger() {
assertAll("Verify all conditions for a VIP passenger and a premium flight",
() -> assertEquals(true, premiumFlight.addPassenger(james)),
() -> assertEquals(1, premiumFlight.getPassengersSet().size()),
() -> assertEquals(true, premiumFlight.removePassenger(james)),
() -> assertEquals(0, premiumFlight.getPassengersSet().size())
);
}
@DisplayName("Then you cannot add him to a premium flight more than once")
@RepeatedTest(5)
public void testPremiumFlightVipPassengerAddedOnlyOnce(RepetitionInfo repetitionInfo) {
for (int i = 0; i < repetitionInfo.getCurrentRepetition(); i++) {
premiumFlight.addPassenger(james);
}
assertAll("Verify a VIP passenger can be added to a premium flight only once",
() -> assertEquals(1, premiumFlight.getPassengersSet().size()),
() -> assertTrue(premiumFlight.getPassengersSet().contains(james)),
() -> assertTrue(new ArrayList<>(premiumFlight.getPassengersSet()).get(0).getName().equals("James"))
);
}
}
}
}
import java.util.*;
public abstract class Flight {
private String id;
Set<Passenger> passengers = new HashSet<>();
public Flight(String id) {
this.id = id;
}
public String getId() {
return id;
}
public Set<Passenger> getPassengersSet() {
return Collections.unmodifiableSet(passengers);
}
public abstract boolean addPassenger(Passenger passenger);
public abstract boolean removePassenger(Passenger passenger);
}
TDD風格:先寫測試用例再實作業務邏輯,測試覆寫率100%。