“Okay, this time I am sure I have covered all possibilities for this function !”
Me, every time I find a new behaviour for the function I am testing
As a developer, I can say that unitary tests is a part of my daily life. For any line of code I am writing/updating, I have to create/update at least one.
As I am mostly using Scala and functional programing, all the following examples will be written in Scala. But the logic here can apply to every languages.
What is an Unitary Test (UT) ?
As its name implies, a UT is here to test a part of the code (one function usually), independently from the whole project. From here we can see they are mainly used from the technical side, and they seem pretty far away from the functional side. That is why having UTs is a very good first step, but they are not enough if we want to check the behaviour of the project as a whole. We will come back to this point later.
As we just said, a UT is here for testing a function. Hence, for this same function we are testing, we can have a limited range of tests, but we can also have a huge number depending on the behaviour of the function.
How to write an UT ?
I like to follow Test-Driven Development (TDD) pattern when I work on features. If you don’t know about it, to sum up TDD is to write the tests before writing the function to test. This can be disturbing if you are not used to work like that, but it is a really good way to write functions that will handle most of the possible cases.
One of the first thing to do, before writing the test, is to be clear about the scope of this test. One think I really like about functional programing is the concept of Pure Function. If you don’t know about this concept, I invite you to read this article https://docs.scala-lang.org/overviews/scala-book/pure-functions.html.
Thanks to this concept, we can easily have the scope of our function : with one or more input, we have one output.
Once we have the scope, we can move to the second point. We know what the function will produce if everything goes well. But what if something goes wrong ? And the most important point is : How can it go wrong ?
Here we can see there is at least 2 parts for the testing :
- The “happy” cases (wanted behaviour)
- The “sad” cases (unwanted behaviour)
Nothing better than an example
Context : We are building a solution for a company selling products online. We will focus on the function that is calculating the price of an order, depending on the price of an item, and the quantity of items wanted by the customer.
Here, the scope is easy to define : we have the item price, the quantity of items wanted, and the final price returned. So everything that will happen, will come from the two input parameters. First point checked !
We now have to think about what could happen in the function. The happy case is pretty easy: if the client wants 2 items, the function will return twice the price of the item.
But what could go wrong here ? What if the price of the item is zero, or negative ? What if the quantity is negative ? Do not forget we are working for a company wanting to sell products, not give them for free ! So we will have to manage errors.
Let’s write our UTs now ! We will use the Either structure of Scala to return either an error (Left), or the valid result (Right).
We will start with the happy case :
test("calculate price by item when price and quantity are positive numbers") {
// Given
val itemPrice = 1.5d
val quantity = 2
// When
val result = calculatePriceByItem(itemPrice, quantity)
// Then
result.isRight shouldBe true
result.value shouldBe 3d
}
And here are our sad cases :
test("calculate price by item when price is zero and quantity is positive") {
// Given
val itemPrice = 0d
val quantity = 2
// When
val result = calculatePriceByItem(itemPrice, quantity)
// Then
result.isLeft shouldBe true
result.value.message shouldBe "Price can not be zero"
}
test("calculate price by item when price is negative and quantity is positive") {
// Given
val itemPrice = -1.5d
val quantity = 2
// When
val result = calculatePriceByItem(itemPrice, quantity)
// Then
result.isLeft shouldBe true
result.value.message shouldBe "Price can not be negative"
}
test("calculate price by item when price is positive and quantity is negative") {
// Given
val itemPrice = 1.5d
val quantity = -2
// When
val result = calculatePriceByItem(itemPrice, quantity)
// Then
result.isLeft shouldBe true
result.value.message shouldBe "Quantity can not be negative"
}
Now we have covered all the identified scenarios, we can write the function :
def calculatePriceByItem(itemPrice: Double, quantity: Integer): Either[OrderError, Double] = {
if (itemPrice == 0)
Left(OrderError("Price can not be zero"))
else if (itemPrice < 0)
Left(OrderError("Price can not be negative"))
else if (quantity < 0)
Left(OrderError("Quantity can not be negative"))
else
Right(itemPrice * quantity)
}
Be careful of the trap of over-testing
This is something that happened to me : wanting to create UTs on all the functions developed. Sometimes you will have top-level function, calling sub-level functions, calling sub-level functions, … If you write a UT for all functions, then when you will update one of the sub-level function, you may have to also update all higher-level functions calling it. In the end, what was supposed to be just a simple update on a feature will transform into a nightmare of updating almost all the tests. The key to avoid this situation is to find the right level of testing.
UT is not the only way to test
As seen before, UT is used for a part of the code, for a function. But it does not guarantee that all functions will work well together. That is the role of Integration Tests (IT) which allow us to create end-to-end tests.
ITs can be used to test the project as a whole, from a more functional point of view. Hence, they are often written with the help of the Business Analyst, who has more knowledge functionally than the developer. Don’t forget that testing is not the responsability of the developer, but the responsability of the team. Don’t hesitate to involve everyone !
We will go into more details about Integration Tests in the next article.