Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Functions for nothing, and your tests for free

Functions for nothing, and your tests for free

An intro to property-based testing, and using F# for testing.

Presented at the ALT.NET Melbourne meetup.

George Pollard

January 30, 2018
Tweet

More Decks by George Pollard

Other Decks in Programming

Transcript

  1. Functions for nothing, and your tests for free } {

    F# FsCheck & property-based testing
  2. : • What are property-based tests? • What do Hedgehog/FsCheck

    do? • Applying property-based testing • Patterns • What not to do • F# for testing • Demo: model-based testing
  3. • What are property-based tests? • What do Hedgehog/FsCheck do?

    • Applying property-based testing • Patterns • What not to do • F# for testing • Demo: model-based testing
  4. [Fact] public void TestUppercase() { var input = "hello"; Assert.Equal("HELLO",

    input.ToUpper()); } [<Fact>] let testUppercase() = let input = "hello" Assert.Equal("HELLO", input.ToUpper())
  5. [Theory] [InlineData("hello", "HELLO")] public void TestUppercase(string input, string output) {

    Assert.Equal(output, input.ToUpper()); } [<Theory>] [<InlineData("hello", "HELLO")>] let testUppercase (input : string) (output : string) = Assert.Equal(output, input.ToUpper())
  6. [Theory] [InlineData("hello", "HELLO")] public void TestUppercase(string input, string output) {

    Assert.Equal(output, input.ToUpper()); } [<Theory>] [<InlineData("hello", "HELLO")>] let testUppercase (input : string) (output : string) = Assert.Equal(output, input.ToUpper())
  7. [Theory] [InlineData("hello", "HELLO")] public void TestUppercase(string input, string output) {

    Assert.Equal(output, input.ToUpper()); } [<Theory>] [<InlineData("hello", "HELLO")>] let testUppercase (input : string) (output : string) = Assert.Equal(output, input.ToUpper())
  8. public void TestUppercase(string input) { var output = input.ToUpper(); Assert.DoesNotContain(output,

    char.IsLower); } let testUppercase (input : string) = let output = input.ToUpper() Assert.DoesNotContain(output, Char.IsLower)
  9. public void NoLowercaseInToUpperResult(string input) { var output = input.ToUpper(); Assert.DoesNotContain(output,

    char.IsLower); } let noLowercaseInToUpperResult (input : string) = let output = input.ToUpper() Assert.DoesNotContain(output, Char.IsLower)
  10. • What are property-based tests? • What do Hedgehog/FsCheck do?

    • Applying property-based testing • Patterns • What not to do • F# for testing • Demo: model-based testing
  11. What do they provide?: A way to generate (and shrink)

    random test data Integration with test runner
  12. public void NoLowercaseInToUpperResult(string input) { var output = input.ToUpper(); Assert.DoesNotContain(output,

    char.IsLower); } let noLowercaseInToUpperResult (input : string) = let output = input.ToUpper() Assert.DoesNotContain(output, Char.IsLower)
  13. [Property] public void ToUpperLeavesNoLowercase(NonNull<string> input) { var output = input.Get.ToUpper();

    Assert.DoesNotContain(output, char.IsLower); } [<Property>] let noLowercaseInToUpperResult (input : NonNull<string>) = let output = input.Get.ToUpper() Assert.DoesNotContain(output, Char.IsLower)
  14. [Property] public void ToUpperLeavesNoLowercase(NonNull<string> input) { var output = input.Get.ToUpper();

    Assert.DoesNotContain(output, char.IsLower); } [<Property>] let noLowercaseInToUpperResult (input : NonNull<string>) = let output = input.Get.ToUpper() Assert.DoesNotContain(output, Char.IsLower)
  15. [Property] public void ToUpperLeavesNoLowercase(NonNull<string> input) { var output = input.Get.ToUpper();

    Assert.DoesNotContain(output, char.IsLower); } [<Property>] let noLowercaseInToUpperResult (input : NonNull<string>) = let output = input.Get.ToUpper() Assert.DoesNotContain(output, Char.IsLower)
  16. Writing your own generator: C#: static Gen<string> FoodGenerator = Gen.Elements("fish",

    "carrot", "shoes"); static Gen<Dog> DogGenerator = from name in Arb.Generate<string>() from colours in Gen.ArrayOf(Arb.Generate<Color>()) from food in FoodGenerator select new Dog(name, colours, food);
  17. Writing your own generator: F#: let foodGenerator = Gen.elements [

    "fish"; "carrot"; "shoes" ] let dogGenerator = gen { let! name = Arb.generate<string> let! colours = Gen.arrayOf Arb.generate<Color> let! food = foodGenerator return Dog(name, colours, food) }
  18. ^[a-z0-9]{3,24}$ 1. Start of string, 2. alphanumeric characters 3. …

    3-24 times, 4. end of string… possibly preceded by a newline.
  19. • What are property-based tests? • What do Hedgehog/FsCheck do?

    • Applying property-based testing • Patterns • What not to do • F# for testing • Demo: model-based testing
  20. Applying property-based testing: How can I use it with my

    existing code? What properties do I test for?
  21. Do: check idempotence: ‘once is enough’ | x.f(…); | x.f(…);

    y.f(…); | Assert(x == y); e.g. Create/Delete
  22. Do: check roundtripping: ‘there and back’ | store(x); | y

    = get(x); | Assert(x == y); e.g. PUT/GET
  23. Do: check invariants: ‘things that don’t change’ | inv(f(x)) ==

    inv(x) Examples: Reverse, Select/Map, etc (length)
  24. Do: check commutativity: ‘order doesn’t matter’ | f(x, y) ==

    f(y, x) Examples: Addition, Multiplication, etc Min, Max, etc
  25. Do: Use model-based testing: Create a simpler version that doesn’t

    implement the full specification Use it to check only those parts
  26. Do: Use model-based testing: 1. Describe actions as values 2.

    Randomly generate actions 3. Apply to all implementations 4. Verify real against model
  27. Do not: Re-run failures: Throws away the specific failure Instead:

    Investigate failure, save as new example-based test
  28. Do not: Perform ad-hoc filtering: For conditional tests, don’t filter

    input values yourself Instead: Use the framework support
  29. • What are property-based tests? • What do Hedgehog/FsCheck do?

    • Applying property-based testing • Patterns • What not to do • F# for testing • Demo: model-based testing
  30. class PlayingCard { public PlayingCard(Rank rank, Suit suit) { Rank

    = rank; Suit = suit; } public Rank Rank { get; } public Suit Suit { get; } }
  31. class PlayingCard : IEquatable<PlayingCard> { public PlayingCard(Rank rank, Suit suit)

    { Rank = rank; Suit = suit; } public Rank Rank { get; } public Suit Suit { get; } public bool Equals(PlayingCard other) => other != null && Rank == other.Rank && Suit == other.Suit; public override bool Equals(object obj) => Equals(obj as PlayingCard); public override int GetHashCode() => (Rank.GetHashCode() * 137) ^ Suit.GetHashCode(); }
  32. class PlayingCard : IEquatable<PlayingCard> , IComparable<PlayingCard> { public PlayingCard(Rank rank,

    Suit suit) { Rank = rank; Suit = suit; } public Rank Rank { get; } public Suit Suit { get; } public int CompareTo(PlayingCard other) { var rankCompare = Rank.CompareTo(other.Rank); if (rankCompare != 0) { return rankCompare; } return Suit.CompareTo(other.Suit); } public bool Equals(PlayingCard other) => other != null && Rank == other.Rank && Suit == other.Suit; public override bool Equals(object obj) => Equals(obj as PlayingCard); public override int GetHashCode() => (Rank.GetHashCode() * 137) ^ Suit.GetHashCode(); }
  33. class PlayingCard : IEquatable<PlayingCard> , IComparable<PlayingCard> { public PlayingCard(Rank rank,

    Suit suit) { Rank = rank; Suit = suit; } public Rank Rank { get; } public Suit Suit { get; } public int CompareTo(PlayingCard other) { var rankCompare = Rank.CompareTo(other.Rank); if (rankCompare != 0) { return rankCompare; } return Suit.CompareTo(other.Suit); } public bool Equals(PlayingCard other) => other != null && Rank == other.Rank && Suit == other.Suit; public override bool Equals(object obj) => Equals(obj as PlayingCard); public override int GetHashCode() => (Rank.GetHashCode() * 137) ^ Suit.GetHashCode(); public override string ToString() => $"Rank: {Rank} Suit: {Suit}"; }
  34. class PlayingCard : IEquatable<PlayingCard> , IComparable<PlayingCard> { public PlayingCard(Rank rank,

    Suit suit) { Rank = rank; Suit = suit; } public Rank Rank { get; } public Suit Suit { get; } public int CompareTo(PlayingCard other) { var rankCompare = Rank.CompareTo(other.Rank); if (rankCompare != 0) { return rankCompare; } return Suit.CompareTo(other.Suit); } public bool Equals(PlayingCard other) => other != null && Rank == other.Rank && Suit == other.Suit; public override bool Equals(object obj) => Equals(obj as PlayingCard); public override int GetHashCode() => (Rank.GetHashCode() * 137) ^ Suit.GetHashCode(); public override string ToString() => $"Rank: {Rank} Suit: {Suit}"; public static bool operator ==(PlayingCard left, PlayingCard right) => Equals(left, right); public static bool operator !=(PlayingCard left, PlayingCard right) => !Equals(left, right); public static bool operator <(PlayingCard left, PlayingCard right) => left.CompareTo(right) < 0; public static bool operator >(PlayingCard left, PlayingCard right) => left.CompareTo(right) > 0; public static bool operator >=(PlayingCard left, PlayingCard right) => left.CompareTo(right) >= 0; public static bool operator <=(PlayingCard left, PlayingCard right) => left.CompareTo(right) <= 0; }
  35. class Result<T> { class Success : Result<T> { private T

    _value; // ... } class Failure : Result<T> { private string _value; // ... } } type Result<'t> = | Failure of string | Success of 't
  36. type Result<'t> = | Failure of string | Success of

    ‘t let getPerson (name : string) : Result<Person> = …
  37. type Result<'t> = | Failure of string | Success of

    ‘t let getPerson (name : string) : Result<Person> = … let myFunc () = match getPerson "George" with | Failure msg -> … | Success p -> …
  38. type Result<'t> = | Failure of string | Success of

    ‘t let getPerson (name : string) : Result<Person> = … let myFunc () = match getPerson "George" with | Failure "Currently presenting" -> … | Failure msg -> … | Success p -> …
  39. [Fact] public void AccountBalanceIncreasesWhenDepositIsMade() { var bankAccount = new BankAccount();

    bankAccount.Deposit(100); Assert.Equal(100, bankAccount.Balance); }
  40. [Fact] public void AccountBalanceIncreasesWhenDepositIsMade() { var bankAccount = new BankAccount();

    bankAccount.Deposit(100); Assert.Equal(100, bankAccount.Balance); }
  41. [<Fact>] let ``Account balance increases when deposit is made`` ()

    = let account = deposit emptyAccount 100 Assert.Equal(100, account.balance)
  42. [<Fact>] let ``Great test `` () = let account =

    deposit emptyAccount 100 Assert.Equal(100, account.balance)