The two methods we tested have no state, so all we had to do was think of inputs and outputs. In object-oriented systems, classes have state. Imagine a ShoppingCart
class and a behavior totalPrice()
that requires some CartItem
s to be inserted before the method can do its job. How do we apply specification-based testing in this case? See the following listing.
Listing 2.14 ShoppingCart
and CartItem
classes
public class ShoppingCart {
private List<CartItem> items = new ArrayList<CartItem>();
public void add(CartItem item) { ❶
this.items.add(item);
}
public double totalPrice() { ❷
double totalPrice = 0;
for (CartItem item : items) {
totalPrice += item.getUnitPrice() * item.getQuantity();
}
return totalPrice;
}
}
public class CartItem { ❸
private final String product;
private final int quantity;
private final double unitPrice;
public CartItem(String product, int quantity,
double unitPrice) {
this.product = product;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
// getters
}
❷ Loops through all the items and sums up the final price
❸ A simple class that represents an item in the cart
Nothing changes in the way we approach specification-based testing. The only difference is that when we reflect about the method under test, we must consider not only the possible input parameters, but also the state the class should be in. For this specific example, looking at the expected behavior of the totalPrice
method, I can imagine tests exercising the behavior of the method when the cart has zero items, a single item, multiple items, and various quantities (plus corner cases such as nulls). All we do differently is to set up the class’s state (by adding multiple items to the cart) before calling the method we want to test, as in the following listing.
Listing 2.15 Tests for the ShoppingCart
class
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class ShoppingCartTest {
private final ShoppingCart cart = new ShoppingCart(); ❶
@Test
void noItems() {
assertThat(cart.totalPrice()) ❷
.isEqualTo(0);
}
@Test
void itemsInTheCart() {
cart.add(new CartItem("TV", 1, 120));
assertThat(cart.totalPrice()) ❸
.isEqualTo(120);
cart.add(new CartItem("Chocolate", 2, 2.5));
assertThat(cart.totalPrice()) ❹
.isEqualTo(120 + 2.5*2);
}
}
❶ Having the cart as a field means we don’t have to instantiate it for every test. This is a common technique to improve legibility.
❷ Asserts that an empty cart returns 0
❸ Asserts that it works for a single item in the cart …
❹ … as well as for many items in the cart.
Again, the mechanics are the same. We just have to take more into consideration when engineering the test cases.
Leave a Reply