Test Driven Development with JUnit 5. Part 6
We move now to the implementation of the PremiumFlight class and its logic. We’ll create PremiumFlight as a subclass of Flight and override the addPassenger and removePassenger methods, but they act like stubs—they do not do anything and simply return false. Their behavior will be extended later. Working TDD style involves creating the tests first and then the business logic.
public class PremiumFlight extends Flight { #1
public PremiumFlight(String id) { #2
super(id); #2
} #2
@Override
public boolean addPassenger(Passenger passenger) { #3
return false; #3
} #3
@Override
public boolean removePassenger(Passenger passenger) { #4
return false; #4
} #4
}
In this listing:
- We declare the PremiumFlight class that extends Flight #1, and we create a constructor for it #2.
- We create the addPassenger #3 and removePassenger #4 methods as stubs, without any business logic. They simply return false.
We now implement the tests according to the premium flight business logic from figures 20.8 and 20.9.
public class AirportTest {
[...]
@DisplayName("Given there is a premium flight") #1
@Nested #1
class PremiumFlightTest { #1
private Flight premiumFlight; #2
private Passenger mike; #2
private Passenger james; #2
@BeforeEach
void setUp() {
premiumFlight = new PremiumFlight("3"); #3
mike = new Passenger("Mike", false); #3
james = new Passenger("James", true); #3
}
@Nested #4
@DisplayName("When we have a regular passenger") #4
class RegularPassenger { #4
@Test #5
@DisplayName("Then you cannot add or remove him #5
from a premium flight") #5
public void testPremiumFlightRegularPassenger() { #5
assertAll("Verify all conditions for a regular passenger #6
and a premium flight", #6
() -> assertEquals(false, #7
premiumFlight.addPassenger(mike)), #7
() -> assertEquals(0, #7
premiumFlight.getPassengersList().size()), #7
() -> assertEquals(false, #8
premiumFlight.removePassenger(mike)), #8
() -> assertEquals(0, #8
premiumFlight.getPassengersList().size()) #8
);
}
}
@Nested #9
@DisplayName("When we have a VIP passenger") #9
class VipPassenger { #9
@Test #10
@DisplayName("Then you can add and remove him #10
from a premium flight") #10
public void testPremiumFlightVipPassenger() { #10
assertAll("Verify all conditions for a VIP passenger #11
and a premium flight", #11
() -> assertEquals(true, #12
premiumFlight.addPassenger(james)), #12
() -> assertEquals(1, #12
premiumFlight.getPassengersList().size()), #12
() -> assertEquals(true, #13
premiumFlight.removePassenger(james)), #13
() -> assertEquals(0, #13
premiumFlight.getPassengersList().size()) #13
);
}
}
}
}
In this listing:
- We declare the nested class PremiumFlightTest #1 that contains the fields representing the flight and the passengers #2 that are set up before each test #3.
- We create two classes nested at the second level in PremiumFlightTest: RegularPassenger #4 and VipPassenger #9. We use the JUnit 5 @DisplayName annotation to label these classes starting with the keyword When.
- We insert one test in each of the newly added RegularPassenger #5 and VipPassenger #10 classes. We label these tests with the JUnit 5 @DisplayName annotation starting with the keyword Then.
- Testing a premium flight and a regular passenger, we use the assertAll method to verify multiple conditions #6. We check that he cannot add a passenger to a premium flight and that trying to add a passenger does not change the size of the passenger list #7. Then, we check that we cannot remove a passenger from a premium flight and that trying to remove a passenger does not change the size of the passenger list #8.
- Testing a premium flight and a VIP passenger, we again use assertAll #11. We check that we can add a passenger to a premium flight and that doing so increases the size of the passenger list #12. Then, we check that we can remove a passenger from a premium flight and that doing so decreases the size of the passenger list #13.
One of the tests is failing now, but this is not a problem. On the contrary: it is what we expected. Remember, working TDD style means being driven by tests, so we first create the test to fail and then write the piece of code that will make the test pass. But there is another remarkable thing here: the test for a premium flight and a regular passenger is already green. This means the existing business logic (the addPassenger and removePassenger methods returning false) is just enough for this case. We understand that we only have to focus on the VIP passenger. To quote Kent Beck again, “TDD helps you to pay attention to the right issues at the right time so you can make your designs cleaner, you can refine your designs as you learn. TDD enables you to gain confidence in the code over time.”
So, we move back to the PremiumFlight class and add the business logic only for VIP passengers. Driven by tests, we get straight to the point.
public class PremiumFlight extends Flight {
public PremiumFlight(String id) {
super(id);
}
@Override
public boolean addPassenger(Passenger passenger) {
if (passenger.isVip()) { #1
return passengers.add(passenger); #1
} #1
return false;
}
@Override
public boolean removePassenger(Passenger passenger) {
if (passenger.isVip()) { #2
return passengers.remove(passenger); #2
} #2
return false;
}
}
In this listing:
- We add a passenger only if the passenger is a VIP #1.
- We remove a passenger only if the passenger is a VIP #2.
The tests run fine and the code coverage is 100%.
Conclusions
This article has covered the following:
- Examining the concept of TDD and demonstrating how it helps us develop safe applications because tests prevent the introduction of bugs into working code and act as part of the documentation.
- Preparing a non-TDD application to be moved to TDD by adding hierarchical JUnit 5 tests that cover the existing business logic.
- Refactoring and improving the code quality of this TDD application by replacing conditional with polymorphism while relying on the tests we developed.
- Implementing new features in by working TDD style, starting by writing tests and then implementing the business logic.