Reputation: 105220
consider this simple example:
Person
objectFirstName
and LastName
(or both, but one is mandatory)Age
(integer, between 0 and 150)How would you property-base-test this simple case?
Upvotes: 1
Views: 165
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