QWAN

Glanceable tests

Tags: test driven development, feedback, eXtreme Programming, refactoring,

As we spend more time reading code than writing it, we’d like our code to be glanceable. By glancing at the code, we want to quickly understand its intent. Glanceability is a useful property, for production code as well as test code. Test code is there to help us out, so being able to quickly grasp what the test is about will help future us, and others, keep our tests habitable.

xeyes screenshot

In the Test Driven Development cycle, we take time for refactoring the code in the last step of the cycle. In this step, we also take time to look at our test code and refactor it when and where necessary. One thing we strive for is glanceable tests.

Glanceable

  1. noting or relating to information on an electronic screen that can be understood quickly or at a glance,
  2. enabling information on a screen to be quickly understood

From: Dictionary.com

In this installment of our Series on TDD heuristics, we’ll discuss some of the micro trade-offs that go into making tests glanceable.

Example

Let’s look at an example from the WeReview conference session management system we have developed: the PlaceSpec test, written in PureScript.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
placeSpec = do
 describe "A Place" do
   it "implements Eq on all its' fields" $
     quickCheck \(r :: Place) -> r === r
   describe "converts itself into human readable text" do
     it "is never empty" do
       quickCheck \(e :: Place) -> (humanReadable e) /== []
   describe "validation" do
     it "fails when no fields have been filled in" do
       shouldFailValidation (default :: Place)
     it "passes when no field is empty" do
       shouldPassValidation noEmptyFields
     it "fails when city is empty" do
       shouldFailValidation $ mkPlace (City "") (Country "non empty country")
     it "fails when country is empty" do
       shouldFailValidation $ mkPlace (City "") (Country "")

This is a fair amount of code, let’s look at some of the aspects that we believe make this more, and in some places less, glanceable.

Some tests don’t require an action

Heuristics are flexible, and made for quick decisions. We make use of that flexibility here, by quickly going against the grain of a heuristice we described earlier: Test name describes action and expected result.

Look at the “it is never empty” test. There is no action and expected outcome, but a relation that always holds. We call this the invariant heuristic. Apologies for the easter egg of a heurstic-in-a-heuristic, we may write about the invarariant heuristic properly at some point.

noEmptyFields is an example of a test that is so glanceable that the test name does not add much.

Seeing this code again, I realise “converts itself” is not a great name. “Human readable text is never empty” is better. In the best Haskell tradition I used a one letter name for the Place value. No clue why I called it e now. Maybe for expected value? Let’s name it place, that reads better in the test’s call site:

1
2
3
   describe "Human readable text" do
     it "is never empty" do
       quickCheck \(place :: Place) -> (humanReadable place) /== []

The inside is not that glanceable, especially if you haven’t seen a property based test before, but at least we can understand the outside. Let us try to read this code together.

How can we read this? A developer new to a team, when we read code together, insisted on naming all the operators in words. This turned out to be enlightening, and I thought I should do that more often (and I still should). Maybe we should have a pronounce the operators heuristic for code reading.

What do we see here? The \== operator is specific to the quickCheck library I use, so we could name it ‘never equals’. Now we read ‘for all generated places, a humanReadable place never equals the empty list’.

Extract for glanceability

We extracted two oneliners for pass and fail validation into functions. One could wonder whether it was worth extracting these two, because they are very short. We believe these extractions make the code more glanceable:

1
2
3
shouldFailValidation e = isValid e `shouldEqual` false

shouldPassValidation e = isValid e `shouldEqual` true

The function names reveal intent at the places where it is used, even though there is very little code inside of it.

If we were to inline this, we’d have to read the inside everywhere. A few symbols don’t hurt so much, but it adds up and eventually it becomes line noise. Fewer moving parts aid in understanding code, and that includes tests. We do have to make sure that the function has a clear name in the context where it is used, otherwise these extractions hurt more than they help.

An Extract Method refactoring is often more about the place where the method is called, than the place where it is defined.

We prioritize making the whole more readable, even when we have to introduce more parts.

One of the goals of encapsulation is letting the whole pay for the price of the parts. If we don’t need to look inside functions, because the name and parameters are clear, then it matters less how many layers there are.

Don’t use an abstraction you already have when it is a bad fit

One of the tests above is about failing validation when some fields are empty:

1
2
     it "fails when country is empty" do
       shouldFailValidation $ mkPlace (City "") (Country "")

At first I considered re-using the abstraction invalidPlace we already had lying around instead of spelling out the empty City and Country. invalidPlace is defined as follows:

1
2
invalidPlace :: Place
invalidPlace = mkPlace (City "") (Country "")

As you can see in the test above, we have exactly two different tests now, one for empty city and one for empty country. If we were really worried, we could follow the logic and make four of them. Having invalidPlace would make it less clear how the Place is not valid. These trade-offs often take some iteration to get right.

Some extractions make sense in a larger context

Moving on from the example above, we look at a test for a client of Place, the poorly named ExpenseRequestDetail.

The tests for expense request and its detail follow a similar pattern as the tests for Place, with shouldFailValidation, objects for empty and nonEmpty, etc.

Similar patterns make glanceability repeatable.

In the test below we have used invalidPlace to construct a larger objects for test. In this larger context, we don’t want to think about what exactly a Place is constructed of, so it makes the test more glanceable:

1
2
it "fails when travellingFrom is not valid" do
   shouldFailValidation ( mkExpenseRequestDetail ( nonEmpty {travellingFrom = invalidPlace}))

We use shouldFailValidation again, but wait, what is nonEmpty?

This made sense at the time of writing the test, as it had three neighbouring it blocks that used nonEmpty with a different field being set to invalid each time. Looking at it again, we noticed having to read and re-read the blocks together in order to make sense of a single one. That is not particularly glanceable. We may have missed an opportunity for better naming. We are tempted to show you what nonEmpty is made of, but that would deny us the opportunity to improve said naming…

1
2
3
it "fails when travellingFrom is not valid" do
   shouldFailValidation ( mkExpenseRequestDetail ( 
       nonEmptyExpenseRequestDetailData {travellingFrom = invalidPlace}))

We renamed nonEmpty to nonEmptyExpenseRequestDetailData, but now the method is so long that it is no longer glanceable. Let’s have a look at our data structure:

1
2
3
4
5
6
7
8
nonEmpty ::  { travellingFrom :: Place
    , travellingTo :: Place
    , pleaseExplain :: String
    }
nonEmpty =
  {travellingFrom: validPlace,
   travellingTo: validPlace,
   pleaseExplain: "explanation"}

Three fields, and we wrote three tests for each field’s invalid state. We use PureScript’s record update syntax to change one value in turn. The non empty expense request detail starts out its life as a valid object. In other languages we might have used a Test Data Builder.

After some back and forth, we settled on validExpenseRequestDetailParameters for now. Not shorter, but slightly less bad than what we had:

1
2
3
it "fails when travellingFrom is not valid" do
   shouldFailValidation ( mkExpenseRequestDetail ( 
       validExpenseRequestDetailParameters {travellingFrom = invalidPlace}))

Why is the glanceability here less than desired?

Notes on the synthesis of poorly named things

When Willem showed this code to Marc, he asked: “why is expenseRequestDetail called an expenseRequestDetail?”

Willem’s response was that he had an expense request, and it needed more details, where previously it didn’t have many. And he couldn’t come up with a more meaningful name at the time. Willem said: “The stakeholders wanted to get some idea of expense requester’s itinerary”, and people may want to explain more, so there is an explanation field. Willem made it and named it as best as he could at the time, and moved on. Hearing himself say Itinerary, this may have been a better name in hindsight.

It is never too late to rename

Now we can use the new name Itinerary in our spec:

1
2
3
it "fails when travellingFrom is not valid" do
   shouldFailValidation ( mkItinerary ( 
       validItineraryValues {travellingFrom = invalidPlace}))

Enough code already.

These are just some of my glanceable things

  • Intention revealing names: test descriptions and function names communicatie intent rather than implementation.
  • Composed method: having all lines of code in a test at the same level of abstraction.
  • Helpers that hide implementation details and express intent through good names, like custom matchers and custom assertions.
  • Hidden setup details, usually the Given part of the test, in a setup, beforeEach or injecting initial state.
  • Exposed relevant data expected values in a test should be understandable without cross-referencing.
  • Wisely (un)used annotations: the test body should be self-contained.
  • Test data builders express different configurations of domain objects clearly and concisely.

Effects

Glanceability of tests supports us when a test fails: we will quickly understand the test’s intent, so that we can fix it rightaway.

Glanceable tests are a form of living documentation: they will read as a specification of the production code. This facilitates understanding of the expected behaviour of the code, and being able to change or add new behaviour.

Writing a glanceable test also helps us in our thinking process. It’s like we are trying to tell a story to someone else - our colleagues, our future selves.

Further reading

We learned a number of heuristics from Kent Beck’s classic book Smalltalk Best Practice Patterns:

  • Composed method - have all lines of code in a method at the same level of abstraction.
  • Intention revealing message - name the method or function for the context in which it is used.
  • Intention revealing selector - name the method or function for its meaning, not for its internal mechanics.

Re-reading the code, and discussing it with Marc surfaced assumptions I made when writing the original code on my own. The code, and my understanding of it improved as a consequence of taking it out of its production context and discussing it.

Rebecca Wirfs-Brock wrote in Principles in Practice (found via this tweet):

Echoing the sentiments of the military strategist Carl von Clausewitz, “Principles and rules are intended to provide a thinking man [or woman, in my case] with a frame of reference.” “I find it refreshing to occasionally step back to deeply examine why one design option seems better than another. I get uneasy when tribal knowledge about “the way things work around here” or vague, hard-to-express sentiments are the only reasons for a particular decision. I guarantee that if you discuss with your colleagues the nuanced reasons for making a particular design choice, you’ll learn more about putting design principles into practice. “

In Effective Unit Testing book, Lasse Koskela describes a number of testing smells that concern readability and maintainability of unit tests.

Eyes pictures credits: Linux xeyes tool.

This is a post in our series on Test Driven Development.

xeyes screenshot

Subscribe to our RSS feed