Pablo Fernandez
Pablo Fernandez

Reputation: 105220

property-based-test for simple object validation

consider this simple example:

How would you property-base-test this simple case?

Upvotes: 1

Views: 165

Answers (1)

Mark Seemann
Mark Seemann

Reputation: 233150

I don't think you can meaningfully answer this question in a language-agnostic way, as the overall design approach will entirely depend on the capabilities of the language in question.

For example, in languages with strong static types and sum types, most of the above requirements can be modelled declaratively using the type system. Here's an F# example:

type Name =
| FirstName of string
| LastName of string
| FullName of string * string

This Name type can only contain either a first name, or a last name, or both. It's not possible to create values that don't follow the requirements.

The case constructor of the following Age type can be hidden by putting the type in a separate module. If that module only exports the toAge (and getAge) function below, the only way to create an Age value would be to call toAge.

type Age = Age of int

let toAge x =
    if 0 <= x && x <= 150
    then Some (Age x)
    else None

let getAge (Age x) = x

Using these auxiliary types, you can now define a Person type:

type Person = { Name : Name; Age : Age }

Most of the requirements are embedded in the type system. You can't create an invalid value of the Person type.

The only behaviour that can fail is contained in the toAge function, so that's the only behaviour that you can meaningfully subject to property-based testing. Here's an example using FsCheck:

open System
open FsCheck
open FsCheck.Xunit
open Swensen.Unquote

[<Property(QuietOnSuccess = true)>]
let ``Value in valid age range can be turned into Age value`` () =
    Prop.forAll (Gen.choose(0, 150) |> Arb.fromGen) (fun i ->
        let actual = toAge i
        test <@ actual |> Option.map getAge |> Option.exists ((=) i) @>)

[<Property(QuietOnSuccess = true)>]
let ``Value in invalid age range can't be turned into Age value`` () =
    let tooLow = Gen.choose(Int32.MinValue, -1)
    let tooHigh = Gen.choose(151, Int32.MaxValue)
    let invalid = Gen.oneof [tooLow; tooHigh] |> Arb.fromGen
    Prop.forAll invalid (fun i ->

        let actual = toAge i

        test <@ actual |> Option.isNone @>)

As you can tell, it tests the two cases: valid input values, and invalid input values. It does this by defining generators for each of those cases, and then verifying the actual values.

Upvotes: 4

Related Questions