Test-driven development
主要是通过不断循环以下两个小步骤将需求一步步实现:
实际上是这种开发循环[test, code, refactor, (repeat)
TDD 三大优点:
通过下面的需求实现过程说明TDD的开发过程
航班添加乘客业务逻辑:经济航班可以加任何类型乘客,商业航班只加VIP乘客
移除乘客逻辑:可以任意移除普通乘客,不允许移除VIP乘客
第一版设计
Fight类
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Flight {
private String id;
private List passengers = new ArrayList();
private String flightType;
public Flight(String id, String flightType) {
this.id = id;
this.flightType = flightType;
}
public String getId() {
return id;
}
public List 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
org.junit.jupiter
junit-jupiter-api
5.6.0
test
org.junit.jupiter
junit-jupiter-engine
5.6.0
test
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 passengers = new ArrayList();
public Flight(String id) {
this.id = id;
}
public String getId() {
return id;
}
public List 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 passengers = new HashSet<>();
public Flight(String id) {
this.id = id;
}
public String getId() {
return id;
}
public Set getPassengersSet() {
return Collections.unmodifiableSet(passengers);
}
public abstract boolean addPassenger(Passenger passenger);
public abstract boolean removePassenger(Passenger passenger);
}
TDD风格:先写测试用例再实现业务逻辑,测试覆盖率100%。
页面更新:2024-04-25
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号