Types + Properties = Software: properties for the advantage state by Mark Seemann
An example of Property-Based Test-Driven Development.
This article is the third in a series of articles that demonstrate how to develop software using types and properties. In the previous article, you saw how to get started with Property-Based Testing, using a Test-Driven Development tactic. In this article, you'll see the previous Tennis Kata example continued. This time, you'll see how to express properties for the state when one of the players have the advantage.
The source code for this article series is available on GitHub.
Winning the game #
When one of the players have the advantage in tennis, the result can go one of two ways: either the player with the advantage wins the ball, in which case he or she wins the game, or the other player wins, in which case the next score is deuce. This implies that you'll have to write at least two properties: one for each situation. Let's start with the case where the advantaged player wins the ball. Using FsCheck, you can express that property like this:
[<Property>] let ``Given advantage when advantaged player wins then score is correct`` (advantagedPlayer : Player) = let actual : Score = scoreWhenAdvantage advantagedPlayer advantagedPlayer let expected = Game advantagedPlayer expected =! actual
As explained in the previous article, FsCheck will interpret this function and discover that it'll need to generate arbitrary Player values for the advantagedPlayer
argument. Because illegal states are unrepresentable, you're guaranteed valid values.
This property calls the scoreWhenAdvantage
function (that doesn't yet exist), passing advantagedPlayer
as argument twice. The first argument is an indication of the current score. The scoreWhenAdvantage function only models how to transition out of the Advantage case. The data associated with the Advantage case is the player currently having the advantage, so passing in advantagedPlayer as the first argument describes the current state to the function.
The second argument is the winner of the ball. In this test case, the same player wins again, so advantagedPlayer is passed as the second argument as well. The exact value of advantagedPlayer doesn't matter; this property holds for all (two) players.
The =!
operator is a custom operator defined by Unquote, an assertion library. You can read the expression as expected must equal actual.
In order to pass this property, you can implement the function in the simplest way that could possibly work:
let scoreWhenAdvantage advantagedPlayer winner = Game advantagedPlayer
Here, I've arbitrarily chosen to return Game advantagedPlayer
, but as an alternative, Game winner
also passes the test.
Back to deuce #
The above implementation of scoreWhenAdvantage is obviously incorrect, because it always claims that the advantaged player wins the game, regardless of who wins the ball. You'll need to describe the other test case as well, which is slightly more involved, yet still easy:
[<Property>] let ``Given advantage when other player wins then score is correct`` (advantagedPlayer : Player) = let actual = scoreWhenAdvantage advantagedPlayer (other advantagedPlayer) Deuce =! actual
The first argument to the scoreWhenAdvantage function describes the current score. The test case is that the other player wins the ball. In order to figure out who the other player is, you can call the other
function.
Which other
function?
The function you only now create for this express purpose:
let other = function PlayerOne -> PlayerTwo | PlayerTwo -> PlayerOne
As the name suggests, this function returns the other player, for any given player. In the previous article, I promised to avoid point-free style, but here I broke that promise. This function is equivalent to this:
let other player = match player with | PlayerOne -> PlayerTwo | PlayerTwo -> PlayerOne
I decided to place this function in the same module as the scoreWhenGame function, because it seemed like a generally useful function, more than a test-specific function. It turns out that the tennis score module, indeed, does need this function later.
Since the other
function is part of the module being tested, shouldn't you test it as well? For now, I'll leave it uncovered by directed tests, because it's so simple that I'm confident that it works, just by looking at it. Later, I can return to it in order to add some properties.
With the other
function in place, the new property fails, so you need to change the implementation of scoreWhenAdvantage in order to pass all tests:
let scoreWhenAdvantage advantagedPlayer winner = if advantagedPlayer = winner then Game winner else Deuce
This implementation deals with all combinations of input.
It turned out that, in this case, two properties are all we need in order to describe which tennis score comes after advantage.
To be continued... #
In this article, you saw how to express the properties associated with the advantage state of a tennis game. These properties were each simple. You can express each of them based on any arbitrary input of the given type, as shown here.
Even when all test input values are guaranteed to be valid, sometimes you need to manipulate an arbitrary test value in order to describe a particular test case. You'll see how to do this in the next article.
If you're interested in learning more about Property-Based Testing, you can watch my introduction to Property-based Testing with F# Pluralsight course.