How does this work with classes and state?

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 CartItems 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
}

❶ Adds items to the cart

❷ 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.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *