fbpx

What are Clean Tests?

The Clean Code rules apply equally to the production code and the test code. Therefore you should clean your test code every time when you write or modify tests. As a result, you will begin to notice opportunities for performing refactoring right after adding a new test into your legacy code. Sometimes you will notice it even before writing the test. This will be the case when a new test requires parts that are already included in other tests – such as existing 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 easy 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, steam-bath, swimming pool). The logic also includes the calculation of loyalty points that each client collects.

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.

@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.getHtmlReceipt();

  // 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 legacy code in small steps

Formatting

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 and when it isn’t like below?

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

@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. Below you will find the metod.

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);
}

As a result, local variables are no longer needed and we can inline them. Below is the result of this code transformation.

@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.However, existing four parameters belong to two groups. The first group is the input data that consists of the first parameter. The second group contains remaining parameters of each assertion. Additionally, if the parameters next to each other are of the same type, it is easy to get confused in their order.

[FM_form id=”1″]

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.

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. It was a side effect of automated refactoring that I’ve used. Therefore I will inline this field. Then the new assertion object will be recreated from scratch wherever the field was used by logic.

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

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 used in one place. I add a new parameter in constructorto populate a new class’s field. When the method uses the “payment” field instead of the “payment” parameter – I can delete the unused assertion’s method parameter.

Replace the constructor with a static method

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

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.

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

@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. It’s visible that the returned facility name is always the same. This way we can check the name separately and only once.

@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");
}

Therefore you can see that we’ve 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. Firstly it follows fluent builder pattern and allows to apply one assertion at a time. Further – in case of test failure – it facilitates writing one detailed message. Finally it can also return a different assertion class if needed.

Take care of tests readability

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

The concepts of refactoring pyramid and SOLID principles are equally applicable when cleaning tests. They help to procede with refactoring a lot.

Additionally I recommend an article about Fluent Interface by Marcin Fowler. You will find more benefits of this design here.

Spread the word. Share this post!