Click here to Skip to main content
15,892,059 members
Articles / .NET

Property-based Tests and Clean Architecture are Perfect Fit

Rate me:
Please Sign up or sign in to vote.
4.00/5 (1 vote)
6 Jun 2023CPOL2 min read 5.5K   1   1
The article provides an example of applying property-based testing on a pure functional domain.
Property-based testing is a technique that allows testing pure functions totally instead of relying on arbitrary values. This article highlights how it can be applied to the domain layer of a clean architecture.

Introduction

A lot has been written about clean architecture. Its main value is the ability to 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 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.

F#
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)

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.

F#
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

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.

History

  • 6th June, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader
Ukraine Ukraine
Team leader with 8 years of experience in the industry. Applying interest to a various range of topics such as .NET, Go, Typescript and software architecture.

Comments and Discussions

 
QuestionYou and I know that Russia is a terrorist state ! Pin
freddie200013-Jul-23 21:29
freddie200013-Jul-23 21:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.