Pozostając w tematyce testów (zachęcam do przeczytania ostatniego wpisu 6 powodów, dla których warto pisać testy) chciałbym przedstawić wzorzec, który ostatnio poznałem i bardzo mi się spodobał. Rozwiązuje on naprawdę ciekawy problem. Deweloper pisząc testy weryfikuje głównie detale techniczne zamiast biznes, który się pod nimi kryje. Przyjrzyjmy się zatem następującemu problemowi.

Studium przypadku

Załóżmy, że tworzymy aplikację do robienia zakupów internetowych. Nasz użytkownik po wybraniu dwóch produktów chce dokonać zakupu. Musimy sprawdzić czy faktycznie w zamówieniu znajdują się dwa produkty oraz czy ich sumaryczna kwota jest prawidłowa. Dodatkowo należy zweryfikować czy został naliczony 10% rabat jeśli zakupy przekroczyły 50zł.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class OrderTest {

  OrderService orderService = new OrderService();

  @Test
  void should_create_order_with_discount() {
    Cart cart = Cart.createCart()
      .withItemWorth(30)
      .withItemWorth(40);

    Order order = orderService.makeOrder(cart);

    assertThat(order.getItems().size()).isEqualTo(2);
    assertThat(order.getSubtotal()).isEqualTo(70);
    assertThat(order.getTotalCost()).isEqualTo(70 - 0.1 * 70);
  }

Test weryfikuje czy po dodaniu dwóch produktów ich ilość w zamówieniu będzie się zgadzała, czy łączna kwota jest prawidłowa oraz czy naliczona została 10% zniżka w końcowej cenie. To znaczy, że ten ten sprawdza wszystko co zostało założone. Jednak patrząc na ten kod można odnieść wrażenie, że weryfikowane są tylko szczegóły techniczne. Jak w tym doszukać się znaczenia biznesowego? Czy taki test można wydrukować na kartkę i pokazać klientowi? Osoby nietechniczne będą miały problem z odczytaniem takiego zapisu. Spróbujmy się temu przyjrzeć i to zmienić.

Chowanie asercji w metodach

Dzięki zastosowaniu wzorca AssertObject jesteśmy w stanie poprawić czytelność tego testu. Tworzymy więc klasę, która będzie posiadała metody zawierające w sobie poszczególne asercje. Spróbujmy przyjrzeć się pierwszemu użyciu tego wzorca.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class OrderTest {

  OrderService orderService = new OrderService();

  @Test
  void should_create_order_with_discount() {
    Cart cart = Cart.createCart()
      .withItemWorth(30)
      .withItemWorth(40);

    Order order = orderService.makeOrder(cart);

    OrderAssertObject assertObject = new OrderAssertObject(order);
    assertObject.hasItemsOf(2);
    assertObject.hasSubtotalOf(70);
    assertObject.hasTotalCostOf(70 - 0.1 * 70);
  }
  ...
}

class OrderAssertObject {

  private final Order order;

  public OrderAssertObject(Order order) {
    this.order = order;
  }

  public void hasItemsOf(int quantity) {
    assertThat(order.getItems().size()).isEqualTo(quantity);
  }

  public void hasSubtotalOf(int subtotal) {
    assertThat(order.getSubtotal()).isEqualTo(subtotal);
  }

  public void hasTotalCostOf(double totalCost) {
    assertThat(order.getTotalCost()).isEqualTo(totalCost);
  }
}

Każda asercja została schowana w odpowiedniej metodzie tylko czy to poprawiło jego czytelność? Mam co do tego wątpliwości, ale na pewno poszliśmy w dobrym kierunku, aby poprawić czytelność. Możemy we wzorcu AssertObject dodatkowo zastosować method chaining dzięki czemu nasz test będzie czytało się jak zdanie czysto biznesowe.

Zwiększenie czytelności, aby pokazać znaczenie testu

Poświęcając niewielki nakład pracy możemy naprawdę osiągnąć niesamowity efekt, spójrzmy!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class OrderTest {

  OrderService orderService = new OrderService();

  @Test
  void should_create_order_with_discount() {
    Cart cart = Cart.createCart()
      .withItemWorth(30)
      .withItemWorth(40);

    Order order = orderService.makeOrder(cart);

    OrderAssertObject.of(order)
      .hasItemsOf(2)
      .hasSubtotalOf(70)
      .hasTotalCostOf(70 - 0.1 * 70);
  }
  ...
}

class OrderAssertObject {

  private final Order order;

  private OrderAssertObject(Order order) {
    this.order = order;
  }

  public static OrderAssertObject of(Order order) {
    return new OrderAssertObject(order);
  }

  public OrderAssertObject hasItemsOf(int quantity) {
    assertThat(order.getItems().size()).isEqualTo(quantity);
    return this;
  }

  public OrderAssertObject hasSubtotalOf(int subtotal) {
    assertThat(order.getSubtotal()).isEqualTo(subtotal);
    return this;
  }

  public OrderAssertObject hasTotalCostOf(double totalCost) {
    assertThat(order.getTotalCost()).isEqualTo(totalCost);
    return this;
  }
}

Czy tak napisany test nie jest czytelniejszy? Przedstawia on dokładnie jakie informacje powinno posiadać zamówienie. Ten fragment kodu z powodzeniem można wydrukować i przedstawić na papierze biznesowi, który powinien go zrozumieć. Jest on świetną podstawą do dyskusji na spotkaniach. Zapraszam Cię dodatkowo do zapoznania się z innym spojrzeniem na AssertObject w tym artykule.

Podsumowanie

Jedynym problemem jaki widzę jest to, że sami musimy zadbać o to, aby stworzyć AssertObject odzwierciedlający potrzeby biznesowe. Stanowi to pewien nakład pracy, aby w odpowiedni sposób dobrać nazwy metod. Najprościej dla programistów jest po prostu użyć biblioteki do testowania i zweryfikować detale techniczne. Jeśli biznes doceni nasz nakład pracy to myślę, że warto stosować AssertObject. W przypadku, gdy jednak będzie to tylko sztuka dla sztuki to może jednak warto sobie odpuścić? Dla mnie osobiście jest to faktycznie uproszczenie, na pierwszy rzut oka widać o co chodzi w danym teście.

Jeśli ten tekst jest dla Ciebie użyteczny to prosiłbym o podzielenie się linkiem do wpisu na Facebooku, Twitterze, LinkedInie bądź w innych mediach społecznościowych. Zachęcam także do dyskusji pod tym artykułem.