TDD测试驱动开发(结合需求一步步实现)
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 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
AirportTestimport 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条件,利用多态特性(运行时而非编译时才确定实际调用具体的方法),使得开发的代码符合**开闭原则,**避免每次增加航班类型都要修改存在的类。
第二版设计
Flightimport 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); }
EconomyFlightpublic 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; } }
BusinessFlightpublic 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; } }
AirportTestimport 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乘客乘坐,其他类型乘客不允许乘坐;可以移除任何类型的乘客。
先实现简单的PremiumFlightpublic 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; } }
AirportTestimport 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实现重复执行。
AirportTestimport 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%。