A guide to clear assertions with Hamcrest

A good practice in test automation is the use of Descriptive And Meaningful Phrases (also known as DAMP). This means that our tests clearly tell us what they do in language that is relevant to the domain. My goal for this article is to guide you through the steps to making descriptive and meaningful assertions with Hamcrest. I will do so by tackling the following issues.

  1. Readability of our assertions
  2. Completeness of the output of our assertions
  3. Duplication of assertion fail information in our tests

Let’s assume we have a class SpeedCamera with a method isPictureTaken which returns a boolean. We’ll skip the setup of the test for brevity’s sake. Let’s write the following JUnit assertion.

public void shouldNotTakePicture() { 
  vehicle.passSpeedCamera(53); 
  assertFalse(speedCamera.isPictureTaken()); 
} 
  // failure output: java.lang.AssertionError 
  // org.junit.Assert.fail(Assert.java:86) .....long stack trace

Readability
Note that the assertion is not very readable and its failure output is incomplete. We’ll take our first step in solving the readability issue with Hamcrest.

public void shouldNotTakePicture() { 
  vehicle.passSpeedCamera(53); 
  assertThat(speedCamera.isPictureTaken(), equalTo(false)); 
} 
  /* failure output: java.lang.AssertionError: 
     Expected: <false>
     but: was <true>

Completeness
Our test reads like a sentence, and even our failure output has improved a bit. But we miss important failure information for our test report. If we were to read the test report, for instance after running a test suite, we would not know which method expected false but was true without having to go back to the test code. We can solve this isue by adding a description to our test like this.

public void shouldNotTakePicture() { 
  vehicle.passSpeedCamera(53);
  assertThat("isPictureTaken should have returned false",
              speedCamera.isPictureTaken(), equalTo(false));
} 
  /*failure output: java.lang.AssertionError: isPictureTaken should have returned false 
    Expected: <false> 
    but: was <true>

Duplication
Our test failure output now tells us which method returns the wrong value. But if we continue to use this approach, we create the problem of duplication of our test failure information as we can see in this example.

public void shouldNotTakePicture() { 
  vehicle.passSpeedCamera(53);
  assertThat("isPictureTaken should have returned false", 
             speedCamera.isPictureTaken(), equalTo(false));
}

public void shouldTakePicture() {
  vehicle.passSpeedCamera(54);
  assertThat("isPictureTaken should have returned true",
             speedCamera.isPictureTaken(), equalTo(true)); 
}

// ad infinitum...

But we can solve the duplication issue by implementing customised BaseMatchers. Let’s assume our SpeedCamera has three methods getCorrectedSpeed, isPictureTaken and isLicenseRevoked. We’ll use the TypeSafeMatcher because it’s straightforward in usage. Check out the Hamcrest API for more options.

public class SpeedCameraMatchers {

  public static BaseMatcher<SpeedCamera> hasCorrectedSpeedTo(final int correctedSpeed) { 
    return new TypeSafeMatcher<SpeedCamera>;() {

      public void describeTo(final Description description) {
        description.appendText("The camera should have corrected the speed to ")
                   .appendValue(correctedSpeed);
      }
    
      public void describeMismatchSafely(final SpeedCamera camera, final Description mismatchDescription) {
        mismatchDescription.appendText(" alas, the camera corrected it to ")
                           .appendValue(camera.getCorrectedSpeed());
      }
    
      public boolean matchesSafely(final SpeedCamera camera) {
        return correctedSpeed == camera.getCorrectedSpeed();
      }
    };
  }

  public static BaseMatcher<SpeedCamera> hasTakenAPicture(final boolean hasTakenPicture) { 
    return new TypeSafeMatcher<SpeedCamera>() {

      public void describeTo(final Description description) {
        description.appendText("Taking a picture should have returned ")
                   .appendValue(hasTakenPicture);
      }
    
      public void describeMismatchSafely(final SpeedCamera camera, final Description mismatchDescription) {
        mismatchDescription.appendText(" returned ")
                           .appendValue(camera.isPictureTaken());
      }
    
      public boolean matchesSafely(final SpeedCamera camera) {
        return hasTakenPicture == camera.isPictureTaken();
      }
    };
  }

  public static BaseMatcher<SpeedCamera> hasRevokedLicense(final boolean hasRevokedLicense) { 
    return new TypeSafeMatcher<SpeedCamera>() {

      public void describeTo(final Description description) {
        description.appendText("Revoking license should have returned ")
                   .appendValue(hasRevokedLicense);
      }
    
      public void describeMismatchSafely(final SpeedCamera camera, final Description mismatchDescription) {
        mismatchDescription.appendText(" returned ")
                           .appendValue(camera.isLicenseRevoked());
      }
    
      public boolean matchesSafely(final SpeedCamera camera) {
        return hasRevokedLicense == camera.isLicenseRevoked();
      }
    };
  } 
} 

Now our assertions are even more readable, their output provides us with complete information and we’re able to reuse them without duplicating code.

public void shouldCorrectSpeedToZero() { 
  vehicle.passSpeedCamera(0); 
  assertThat(speedCamera, hasCorrectedSpeedTo(0)); 
} 
/*failure output: java.lang.AssertionError: 
  Expected: The camera should have corrected the speed to <0> 
  but: alas, the camera corrected it to <-3>*/

public void shouldCorrectSpeedToHundred() { 
  vehicle.passSpeedCamera(105); 
  assertThat(speedCamera, hasCorrectedSpeedTo(100)); 
} 
/*failure output: java.lang.AssertionError: 
  Expected: The camera should have corrected the speed to <100> 
  but: alas, the camera corrected it to <99>*/

public void shouldTakePicture() { 
  vehicle.passSpeedCamera(54); 
  assertThat(speedCamera, hasTakenAPicture(true)); 
} 
/*failure output: java.lang.AssertionError:
  Expected: Taking a picture should have returned <true> 
  but: returned <false>*/

public void shouldNotTakePicture() { 
  vehicle.passSpeedCamera(53); 
  assertThat(speedCamera, hasTakenAPicture(false)); 
} 
/*failure output: java.lang.AssertionError:
  Expected: Taking a picture should have returned <false> 
  but: returned <true>*/

public void shouldRevokeLicense() { 
  vehicle.passSpeedCamera(105); 
  assertThat(speedCamera, hasRevokedLicense(true)); 
} 
/*failure output: java.lang.AssertionError:
  Expected: Revoking license should have returned <true>
  but: returned <false>*/

public void shouldNotRevokeLicense() { 
  vehicle.passSpeedCamera(104);
  assertThat(speedCamera, hasRevokedLicense(false)); 
} 
/*failure output: java.lang.AssertionError: 
  Expected: Revoking license should have returned <false>
  but: returned <true>*/ 

In my next article we will look at a safe way to chain our freshly made custom BaseMatchers.

Author: David Baak

To code or not to code? That's true.

Leave a Reply

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