Core Java

How to do test refactoring towards fluent assertion pattern?

What are Clean Tests?

The Clean Code rules apply equally to the production code and the test code. So do code cleanup every time, including when you write tests. You will often notice opportunities for refactoring right after adding a new test or even before writing it. This will be the case when a new test requires parts that are already included in other tests – such as assertions or system configuration.

Such adjustments should take into account the basic principles of Clean Code. They mainly concern maintaining readability and maintaining the ease of introducing further changes. We should also make sure that the code is quick to read and understand.

Refactoring example

Below is a set of several integration tests. They check the price list for visiting a fitness club (gym, sauna, swimming pool). The logic also includes the calculation of loyalty points.

Although the example of this test is quite short, it already contains some code duplications. Code repeats can be found at the beginning and end of each test case.

01
02
03
04
05
06
07
08
09
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
48
@Test
public void twoHours_isOnly_payEntryFee() {
  Facility beFitGym = new Facility("Be Fit Gym", Facility.GYM);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");
 
  // when
  client.addVisit(visit);
  String payment = client.getReceipt();
 
  // Then
  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[1]")
    .isEqualTo("Be Fit Gym");
         
  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[2]")
    .isEqualTo("4.0");
         
  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[3]")
    .isEqualTo("100");
}
 
@Test
public void twoHours_PayForEach() {
  // Given
  Facility beFitGym = new Facility("Jacuzzi", Facility.STEAM_BATH);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");
 
  // When
  client.addVisit(visit);
  String payment = client.getReceipt();
 
  // Then
  assertThat(payment)
   .valueByXPath("/table/tr[1]/td[1]")
   .isEqualTo("Be Fit Jacuzzi");
         
  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[2]")
    .isEqualTo("10.0");
 
  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[3]")
    .isEqualTo("300");
}

Refactoring in small steps

Formatowanie

Before I do my first transformation, note the value of code formatting. The above code has already been formatted. Before that, it looked like the code below. You probably see the difference when the code is clearer?

1
2
3
4
5
6
7
@Test
public void twoHours_PayForEach() {
  ...
  assertThat(payment).valueByXPath("/table/tr[1]/td[1]").isEqualTo("Gym");
  assertThat(payment).valueByXPath("/table/tr[1]/td[2]").isEqualTo("10.0");
  assertThat(payment).valueByXPath("/table/tr[1]/td[3]").isEqualTo("300");
}

Make assertions dependent on local variables

In well-formatted code, code repeats are more visible. This is how I prepare the code to extract methods that contain repetitions of logic. Before I perform the method extraction, I will make the repeating code dependent on local variables by extracting them.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Test
public void twoHours_payEntryFee() {
  // Given
  Facility beFitGym = new Facility("Be Fit Gym", Facility.GYM);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");
 
  // When
  client.addVisit(visit);
  String payment = client.getReceipt();
 
  // Then
  String facilityName = "Be Fit Gym";
  String facilityPrice = "4.0";
  String facilityPoints = "100";
 
  assertThat(payment)
   .valueByXPath("/table/tr[1]/td[1]")
   .isEqualTo(facilityName);
         
  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[2]")
    .isEqualTo(facilityPrice);
 
  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[3]")
    .isEqualTo(facilityPoints);
}

Extract the assertions method

Now it’s time to extract the method. This is an automatic code refactoring in most Java development environments.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
private void assertFacility(String payment,
    String facilityName,
    String facilityPrice,
    String facilityPoints) {
    
  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[1]")
    .isEqualTo(facilityName);
 
  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[2]")
    .isEqualTo(facilityPrice);
 
  assertThat(payment)
    .valueByXPath("/table/tr[1]/td[3]")
    .isEqualTo(facilityPoints);
}

The extracted local variables are no longer needed, so we can inline them. Below is the result of this test refactoring.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Test
public void twoHours_isOnly_payEntryFee() {
  Facility beFitGym = new Facility("Be Fit Gym", Facility.GYM);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");
 
  // when
  client.addVisit(visit);
  String payment = client.getReceipt();
 
  // Then
  assertFacility(payment, "Be Fit Gym", 4.0, 100);
}
 
@Test
public void twoHours_PayForEach() {
  // Given
  Facility beFitGym = new Facility("Jacuzzi", Facility.STEAM_BATH);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");
 
  // When
  client.addVisit(visit);
  String payment = client.getReceipt();
 
  // Then
  assertFacility(payment, "Jacuzzi", 10.0, 150);
}

Pay attention to the parameters of the methods

Note that the tests have become shorter. The problem, however, is now the number of parameters that additionally belong to two groups. The first group is the input data (the first parameter) and the second group are the values of each assertion (the next three parameters). Additionally, if the parameters next to each other are of the same type, it is easy to get confused in their order.

Create a new assertion class

Next, I will use the above two groups of parameters as the direction for subsequent changes. I put the method in a new class and define one of the groups as a constructor parameter. Then the current method will only contain parameters from the second group and will gain access to the first group through the class fields.

Dokonaj ektrakcji klasy poprzez ekstrakcję delegata

To create a new class, I launch “extract delegate” code refactoring, which is another automated conversion in IntelliJ IDE for Java language.

Here is the result of code transformation.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private final FacilityAssertion facilityAssertion = new FacilityAssertion();
 
@Test
public void twoHours_isOnly_payEntryFee() {
  Facility beFitGym = new Facility("Be Fit Gym", Facility.GYM);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");
 
  // when
  client.addVisit(visit);
  String payment = client.getReceipt();
 
  // Then
  facilityAssertion.assertFacility(payment, "Be Fit Gym", 4.0, 100);
}
 
@Test
public void twoHours_PayForEach() {
  // Given
  Facility beFitGym = new Facility("Jacuzzi", Facility.STEAM_BATH);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");
 
  // When
  client.addVisit(visit);
  String payment = client.getReceipt();
 
  // Then
  facilityAssertion.assertFacility(payment, "Jacuzzi", 10.0, 150);
}

Inline field

The extra field in the class was not my goal. So I am absorbing this field. Then the new assertion object will be recreated from scratch wherever the field was used by logic.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Test
public void twoHours_isOnly_payEntryFee() {
  Facility beFitGym = new Facility("Be Fit Gym", Facility.GYM);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");
 
  // when
  client.addVisit(visit);
  String payment = client.getReceipt();
 
  // Then
  new FacilityAssetion().assertFacility(payment, "Be Fit Gym", 4.0, 100);
}
 
@Test
public void twoHours_PayForEach() {
  // Given
  Facility beFitGym = new Facility("Jacuzzi", Facility.STEAM_BATH);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");
 
  // When
  client.addVisit(visit);
  String payment = client.getReceipt();
 
  // Then
  new FacilityAssetion().assertFacility(payment, "Jacuzzi", 10.0, 150);
}

Then I re-extract the “assertFacility” method. Thanks to this, calling the assertion constructor will be in one place only. Below the refactoring result.

01
02
03
04
05
06
07
08
09
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
private void assertFacility(String payment, String facilityName,
      String facilityPrice, String facilityPoints) {
        new FacilityAssertion()
          .assertFacility(payment, facilityName,
                          facilityPrice, facilityPoints);
    }
 
@Test
public void twoHours_isOnly_payEntryFee() {
  Facility beFitGym = new Facility("Be Fit Gym", Facility.GYM);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");
 
  // when
  client.addVisit(visit);
  String payment = client.getReceipt();
 
  // Then
  assertFacility(payment, "Be Fit Gym", 4.0, 100);
}
 
@Test
public void twoHours_PayForEach() {
  // Given
  Facility beFitGym = new Facility("Jacuzzi", Facility.STEAM_BATH);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");
 
  // When
  client.addVisit(visit);
  String payment = client.getReceipt();
 
  // Then
  assertFacility(payment, "Jacuzzi", 10.0, 150);
}

Move the parameter from the method to the constructor

The constructor (FacilityAssertion) is currently only called from one place. So I add a new parameter in constructor, then a field in this class. When the method uses the “payment” field instead of the “payment” parameter – I can delete the unnecessary parameter.

Replace the constructor with a static method call

Next, in the FacilityAssertion class, I run the automatic code transformation “Replace constructor call with static method”.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class FacilityAssertion {
  private String payment;
 
  private FacilityAssertion(String payment) {
     this.payment = payment;
  }
 
  public static FacilityAssertion assertThat(String payment) {
      return new FacilityAssertion(payment);
  }
 
  void hasAttributes(String facilityName, String facilityPrice,
     String facilityPoints) {
    XmlAssert.assertThat(this.payment)
      .valueByXPath("/table/tr[1]/td[1]")
      .isEqualTo(facilityName);
 
    XmlAssert.assertThat(this.payment)
      .valueByXPath("/table/tr[1]/td[2]")
      .isEqualTo(facilityPrice);
 
    XmlAssert.assertThat(this.payment)
      .valueByXPath("/table/tr[1]/td[3]")
      .isEqualTo(facilityPoints);
  }
}

Replace method with a method chain

Time to build a method chain. So I do the last extraction of a few new methods that will contain “return this” at their ends. This will allow me to make code refactoring of these methods into a call chain.

01
02
03
04
05
06
07
08
09
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
public class FacilityAssertion {
  private String payment;
 
  private FacilityAssertion(String payment) {
    this.payment = payment;
  }
 
  public static FacilityAssertion assertThat(String payment) {
    return new FacilityAssertion(payment);
  }
 
  FacilityAssertion hasAttributes(String facilityName,
    String facilityPrice,
    String facilityPoints) {
      return hasName(facilityName)
              .hasPrice(facilityPrice)
              .hasPoints(facilityPoints);
  }
 
  FacilityAssertion hasPoints(String facilityPoints) {
    XmlAssert.assertThat(this.payment)
      .valueByXPath("/table/tr[1]/td[3]")
      .isEqualTo(facilityPoints);
    return this;
  }
 
  FacilityAssertion hasPrice(String facilityPrice) {
    XmlAssert.assertThat(this.payment)
     .valueByXPath("/table/tr[1]/td[2]")
     .isEqualTo(facilityPrice);
    return this;
  }
 
  FacilityAssertion hasName(String facilityName) {
    XmlAssert.assertThat(this.payment)
     .valueByXPath("/table/tr[1]/td[1]")
     .isEqualTo(facilityName);
    return this;
  }
}

Inline initial assertion method

01
02
03
04
05
06
07
08
09
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
@Test
public void twoHours_isOnly_payEntryFee() {
  Facility beFitGym = new Facility("Be Fit Gym", Facility.GYM);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");
 
  // when
  client.addVisit(visit);
  String payment = client.getReceipt();
 
  // Then
  assertThat(payment)
    .hasName("Be Fit Gym")
    .hasPrice("4.0")
    .hasPoints("100");
}
 
@Test
public void twoHours_PayForEach() {
  // Given
  Facility beFitGym = new Facility("Jacuzzi", Facility.STEAM_BATH);
  Visit visit = new Visit(beFitGym, 2);
  Client client = new Client("Mike");
 
  // When
  client.addVisit(visit);
  String payment = client.getReceipt();
 
  // Then
  assertThat(payment)
    .hasName("Jacuzzi")
    .hasPrice("10.0")
    .hasPoints("150");
}

Use the builder or factory pattern analogously for the test setup

You’ve surely noticed that now the test configurations differ only in the type of facility and the visit duration. The returned facility name is always the same, so you can check it separately and only once.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void twoHours_isOnly_payEntryFee() {
  // Given
  String payment = newPaymentFor(Facility.GYM, 2);
 
  // Then
  assertThat(payment)
    .hasPrice("4.0")
    .hasPoints("100");
}
 
@Test
public void twoHours_PayForEach() {
  // Given
  String payment = newPaymentFor(Facility.STEAM_BATH, 2);
 
  // Then
  assertThat(payment)
    .hasPrice("10.0")
    .hasPoints("150");
}

As you can see, we refactored code above into clean tests. They have no code duplication and are easy to understand. Writing another test is also simple.

Libraries promoting the fluent builder pattern

Fluent assertion pattern is supported by testing libraries. One of them is asserjJ that works very well with JUnit. It follows fluent builder pattern and allow to create one assertion at a time. It facilitates writing one detailed message in case of test failure or returning a new nested assertion instance that checks more.

Take care of tests readability

Uncle Bob once said (or wrote), “treat your tests like a first-class citizen.” So take care of your tests by constantly refactoring them! Clean Code is also Clean Tests!

Remember that the concept of the refactoring pyramid and the SOLID principles are equally applicable in cleaning tests through refactoring.

Published on Java Code Geeks with permission by Wlodek Krakowski, partner at our JCG program. See the original article here: How to do test refactoring towards fluent assertion pattern?

Opinions expressed by Java Code Geeks contributors are their own.

Wlodek Krakowski

Włodek Krakowski is an independent technical trainer specializing in maintaining code quality through refactoring. His main interest is taking care of delivering valued software from different perspectives. He is a founder and executive editor of blog at www.refactoring.pl".
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button