Types + Properties = Software: other properties by Mark Seemann
Even simple functions may have properties that can be expressed independently of the implementation.
This article is the eighth in a series of articles that demonstrate how to develop software using types and properties. In previous articles, you've seen an extensive example of how to solve the Tennis Kata with type design and Property-Based Testing. Specifically, in the third article, you saw the introduction of a function called
other. In that article we didn't cover that function with automatic tests, but this article rectifies that omission.
The source code for this article series is available on GitHub.
Implementation duplication #
other function is used to find the opponent of a player. The implementation is trivial:
let other = function PlayerOne -> PlayerTwo | PlayerTwo -> PlayerOne
Unless you're developing software where lives, or extraordinary amounts of money, are at stake, you probably need not test such a trivial function. I'm still going to show you how you could do this, mostly because it's a good example of how Property-Based Testing can sometimes help you test the behaviour of a function, instead of the implementation.
Before I show you that, however, I'll show you why example-based unit tests are inadequate for testing this function.
You could test this function using examples, and because the space of possible input is so small, you can even cover it in its entirety:
[<Fact>] let ``other than playerOne returns correct result`` () = PlayerTwo =! other PlayerOne [<Fact>] let ``other than playerTwo returns correct result`` () = PlayerOne =! other PlayerTwo
=! operator is a custom operator defined by Unquote, an assertion library. You can read the first expression as player two must equal other than player one.
The problem with these tests, however, is that they don't state anything not already stated by the implementation itself. Basically, what these tests state is that if the input is PlayerOne, the output is PlayerTwo, and if the input is PlayerTwo, the output is PlayerOne.
That's exactly what the implementation does.
The only important difference is that the implementation of
other states this relationship more succinctly than the tests.
It's as though the tests duplicate the information already in the implementation. How does that add value?
Sometimes, this can be the only way to cover functionality with tests, but in this case, there's an alternative.
Behaviour instead of implementation #
Property-Based Testing inspires you to think about the observable behaviour of a system, rather than the implementation. Which properties do the
other function have?
If you call it with a Player value, you'd expect it to return a Player value that's not the input value:
[<Property>] let ``other returns a different player`` player = player <>! other player
<>! operator is another custom operator defined by Unquote. You can read the expression as player must not equal other player.
This property alone doesn't completely define the behaviour of
other, but combined with the next property, it does:
[<Property>] let ``other other returns same player`` player = player =! other (other player)
If you call
other, and then you take the output of that call and use it as input to call
other again, you should get the original Player value. This property is an excellent example of what Scott Wlaschin calls there and back again.
These two properties, together, describe the behaviour of the
other function, without going into details about the implementation.
You've seen a simple example of how to describe the properties of a function without resorting to duplicating the implementation in the tests. You may not always be able to do this, but it always feels right when you can.
If you're interested in learning more about Property-Based Testing, you can watch my introduction to Property-based Testing with F# Pluralsight course.