A glimpse of the path at the beginning of test-driven development
What is test-driven development?
Test-driven development refers to the development method of writing test code before writing implementation code. Kent Beck, the author of JUnit, said: the important reason for writing test-driven code is to eliminate fear and uncertainty in development, because the fear of writing code will let you carefully test and avoid communication, it makes you feel ashamed to get feedback and make you anxious. TDD is an important means to eliminate fear and make Java developers more confident and more willing to communicate. The benefits of TDD may not be immediately presented, but at some point you will find that these benefits include:
TDD can be used at multiple test levels, as shown in the following table:
Test level |
Description |
Unit Test |
Code in the test class |
Integration Test |
Interaction between test classes |
System Test |
Testing System |
System integration test |
The testing system includes third-party components. |
Example of test-driven development
Now we need a piece of code to calculate the ticket income of a movie theater. The current business rules are very simple, including:
- Price per ticket (unit price) ¥30
- Revenue = number of tickets sold * unit price
- The auditorium can accommodate a maximum of 100 people
Here is another assumption: because there is no professional device or system to count the number of tickets sold, when calculating the ticket revenue, the number of tickets sold is manually input by the user.
The basic steps of TDD are: red-green-reconstruction.
Next, follow these steps to complete the Ticket Revenue calculation function.
package com.lovo;import java.math.BigDecimal;import org.junit.Assert;import org.junit.Before;import org.junit.Test;public class TicketRevenueTest { private TicketRevenue ticketRevenue; private BigDecimal expectedRevenue; @Before public void setUp() { ticketRevenue= new TicketRevenue(); } @Test public void oneTicketSoldIsThirtyInRevenue() { expectedRevenue = new BigDecimal("30"); Assert.assertEquals(expectedRevenue, ticketRevenue.estimateTotalRevenue(1)); }}
The above test code does not pass the test, or even the compilation fails, because the TicketRevenue class does not exist. Next, we can use the code repair feature of IDE (both Eclipse and IntelliJ have this feature) to create the TicketRevenue class and the estimateTotalRevenue Method for Calculating the ticket income in this class.
package com.lovo;import java.math.BigDecimal;public class TicketRevenue { public BigDecimal estimateTotalRevenue(int i) { return BigDecimal.ZERO; }}
Now you can run your unit test cases. However, this test is impossible because we have not implemented real business logic, as shown in.
However, we have completed the "red" step so far. Next, we modify the estimateTotalRevenue method of the TicketRevenue class to pass the test.
package com.lovo;import java.math.BigDecimal;public class TicketRevenue { public BigDecimal estimateTotalRevenue(int numberOfTicketsSold) { BigDecimal totalRevenue = BigDecimal.ZERO; if(numberOfTicketsSold == 1) { totalRevenue = new BigDecimal(30); } return totalRevenue; }}
Run the unit test again. The result is shown in.
Here, the second step is "green.
Next, we will refactor the code of the TicketRevenue class.
package com.lovo;import java.math.BigDecimal;public class TicketRevenue { private final static int TICKET_PRICE = 30; public BigDecimal estimateTotalRevenue(int numberOfTicketsSold) { BigDecimal totalRevenue = null; totalRevenue = new BigDecimal(TICKET_PRICE * numberOfTicketsSold); return totalRevenue; }}
The restructured code can calculate the corresponding revenue based on the number of tickets sold. Compared with the previous hard code, it has taken a big step forward, however, it does not take into account the input values smaller than 0 or greater than 100. Therefore, we need more test examples to simulate possible input in the actual working environment. We have made the following improvements to the test code.
package com.lovo;import java.math.BigDecimal;import org.junit.Assert;import org.junit.Before;import org.junit.Test;public class TicketRevenueTest { private TicketRevenue ticketRevenue; private BigDecimal expectedRevenue; @Before public void setUp() { ticketRevenue = new TicketRevenue(); } @Test(expected = IllegalArgumentException.class) public void failIfLessThanZeroTicketsAreSold() { ticketRevenue.estimateTotalRevenue(-1); } @Test public void zeroSalesEqualsZeroRevenue() { Assert.assertEquals(BigDecimal.ZERO, ticketRevenue.estimateTotalRevenue(0)); } @Test public void oneTicketSoldIsThirtyInRevenue() { expectedRevenue = new BigDecimal("30"); Assert.assertEquals(expectedRevenue, ticketRevenue.estimateTotalRevenue(1)); } @Test public void tenTicketsSoldIsThreeHundredInRevenue() { expectedRevenue = new BigDecimal("300"); Assert.assertEquals(expectedRevenue, ticketRevenue.estimateTotalRevenue(10)); } @Test(expected = IllegalArgumentException.class) public void failIfMoreThanOneHundredTicketsAreSold() { ticketRevenue.estimateTotalRevenue(101); }}
If you run the test again, you will find that two tests with invalid input in five tests fail (the sales volume is-1 and 101). The reason is very simple, no invalid input code has been processed in our code. Next we will continue to refactor our code.
Package com. lovo; import java. math. bigDecimal; public class TicketRevenue {private final static int TICKET_PRICE = 30; public BigDecimal limit (int limit) throws IllegalArgumentException {BigDecimal totalRevenue = null; if (Limit <0) {throw new IllegalArgumentException ("the number of tickets sold must be greater than or equal to 0");} else if (numberOfTicketsSold> 100) {throw new IllegalArgumentException ("the number of tickets sold must be less than or equal to 100");} else {totalRevenue = new BigDecimal (TICKET_PRICE * numberOfTicketsSold);} return totalRevenue ;}}
Run the test code again and check whether your bar is green (the famous saying of JUnit is "Keep your bar green "). Of course, for code cleaner, the above Code is still a little bloated, it doesn't matter. Let's rebuild it again.
Package com. lovo; import java. math. bigDecimal; public class TicketRevenue {private final static int TICKET_PRICE = 30; public BigDecimal estimateTotalRevenue (int partition) throws IllegalArgumentException {if (partition <0 | partition> 100) {throw new IllegalArgumentException ("the number of tickets sold must be between 0 and 100");} return new BigDecimal (TICKET_PRICE * numberOfTicketsSold );}}
After you have modified the code, never forget to perform another test. You still need Keep your bar green.
If we use an object-oriented programming paradigm, code refactoring should follow the object-oriented design principles. Robert Matin summarized these principles as the SOLID principle.
Principles |
English |
Description |
Single Responsibility Principle (S) |
Single Responsibility Principle |
Each object only does what it should do. |
Open/closed principle (O) |
Open-Closed Principle |
Accept extended but not modified |
Lee's replacement principle (L) |
Liskov Substitution Principle |
The parent type can be replaced with the child type. |
Interface isolation principle (I) |
Interface Segregation Principle |
Small and specialized Interfaces |
Dependency reversal principle (D) |
Dependency Inversion Principle |
Depends on the interface and does not rely on implementation |
Note:The above example is from The Well-Grounded Java Developer book (Chinese name: "Java programmer cultivation path"). This book covers many practical technologies in Java Development and New Java language features, if you are interested, you can read this book. I believe you will get a lot from it.