DEV Community

Bohdan Stupak
Bohdan Stupak

Posted on

Property-based tests and clean architecture are perfect fit

It has been written a lot about the clean architecture. Its main value is the ability maintain free from side effects domain layer that allows us to test core business logic without leveraging heavy mocks.

However, when it comes to designing tests for out pure domain logic quite often we don't tend to be so picky. Unit testing contains many traps such as overspecified software. But even when it comes to testing pure functions which may seem as a pretty straightforward process we may encounter some pitfalls.

One of them is that when writing unit tests we rely on some sort of arbitrary magic numbers. While we may guarantee that our function works correctly at given points we can't guarantee that it works at every point. An alternative would be to check whether the function satisfies some criteria continuously.

And this is what property-based testing is aimed at. Instead of verifying output at hardcoded input points, it checks the properties of the function you define with a multitude of generated values.

Let's look at the code example to see how that works. Below is the example from my project Kyiv Station Walk. You can see the function that takes a collection of checkpoints from the domain and transforms it so it conforms to the rules of the presentation layer.

let removeRedundantCheckpoints (checkPoints : Location[]) =
    let checkPointsMaxCount = 5
    let isStartOrEndOfTheRoute (checkPoints : Location[]) i =
       i = 0 || i = checkPoints.Length - 1
    let euclidianDistance c1 c2 =
        Math.Pow(float(c1.lattitude - c2.lattitude), float(2)) + Math.Pow(float(c1.longitude - c2.longitude), float(2))
    if checkPoints.Length <= 5 then
        checkPoints
    else
        checkPoints
        |> Array.mapi(fun i c ->
            if isStartOrEndOfTheRoute checkPoints i then
                {
                    index = i
                    checkPoint = c
                    distanceToNextCheckPoint = float(1000000)
                }
            else
                {
                    index = i
                    checkPoint = c
                    distanceToNextCheckPoint = euclidianDistance checkPoints.[i+1] c
                }
        )
        |> Array.sortByDescending(fun i -> i.distanceToNextCheckPoint)
        |> Array.take(checkPointsMaxCount)
        |> Array.sortBy(fun i -> i.index)
        |> Array.map(fun i -> i.checkPoint)
Enter fullscreen mode Exit fullscreen mode

We can supply some arbitrary arrays of checkpoints and check the output or instead we can think about some properties that our function should satisfy. Here are these properties expressed in code.

open FsCheck.Xunit
open RouteModels

module RemoveRedundantCheckpointsTests =

    let ``result array contains no more than 5 items`` input mapFn =
        let res = mapFn input
        Array.length res <= 5

    [<Property>]
    let maxLength x =
        ``result array contains no more than 5 items`` x removeRedundantCheckpoints

    let ``result contains first point from input`` (input: Location[]) (mapFn : Location[] -> Location[]) =
        if Array.length input = 0 then
            true
        else
            let res = mapFn input
            res.[0] = input.[0]

    [<Property>]
    let firstItem x =
        ``result contains first point from input`` x removeRedundantCheckpoints

    let ``result contains last point from input`` (input: Location[]) (mapFn : Location[] -> Location[]) =
        if Array.length input = 0 then
            true
        else
            let res = mapFn input
            res.[res.Length-1] = input.[input.Length-1]

    [<Property>]
    let lastItem x =
        ``result contains last point from input`` x removeRedundantCheckpoints

    let ``result contains only points from input`` input mapFn =
        let res = mapFn input
        Array.length (Array.except input res) = 0

    [<Property>]
    let onlyInput x =
        ``result contains only points from input`` x removeRedundantCheckpoints
Enter fullscreen mode Exit fullscreen mode

As you can see from the imports statement we're relying on FsCheck to generate some random values for us.

Later in the code, we declare a higher-order function that accepts the mapper function and input array and returns a boolean condition that checks whether the property is satisfied. Double backticks is a convenient F# feature that allows us to express property in a natural language.

The test is decorated with Property attribute and accepts input generated by FsCheck as well as removeRedundantCheckpoints function which is subject to change. With such a setup we can check whether the function under tests satisfies provided properties with the multitude of random values generated by a library.

Conclusion

When it comes to testing a lot of teams really put the same effort into designing a test suite as into the application code. And even those who do rarely consider something outside of the traditional testing pyramid. Still, property-based testing represents a nice option for pure logic that usually resides in the domain layer or in the mapping layers of your application.

Top comments (0)