Property-based
Testing
Having over 10 years of experience with
Microsoft Technologies, Miguel has many
specializations, including C#, F#, Azure, and
DevOps practices.
MSDEVMTL co-organizer
Director of engineering at Nexus Innovations
https://blog.miguelbernard.com
https://www.linkedin.com/in/miguelbernard/
@MiguelBernard88
https://github.com/mbernard
State of unit testing
// Given
var input = 3;
// When
var result =
Calculator.Calculate(input);
// Then
Assert.Equal(0, result);
// Given
var input = 3;
// When
var result =
Calculator.Calculate(input);
// Then
Assert.Equal(5, result);
// Given
var input = 3;
// When
var result =
Calculator.Calculate(input);
// Then
Assert.Equal(5, result);
// Given
var input = 3;
// When
var result =
Calculator.Calculate(input);
// Then
Assert.Equal(3, result);
How many
tests are
enough?
I thought I was happy…
Problems with example-based tests (EBT)
DON’T SCALE DON’T HELP YOU TO
FIND EDGE CASES
BRITTLE
DON’T EXPLAIN
WHAT WAS THE
REQUIREMENT
OFTEN SPECIFIC TO
AN
IMPLEMENTATION
What’s Property-based testing?
What it’s not
•
•
•
•
•
•
More like
Property
Property-based test
(PBT)
Example with
ADD()
Commutativity
2+3 = 3+2 2-3 != 3-2 2*3 = 3*2
Associativity
Add(input1, input2)
Add(input2, input3)
(2+3)+4 = 2+(3+4) (2-3)-4 != 2-(3-4) (2*3)*4 = 2*(3*4)
Identity
2+0 = 2 2-0 = 2 2*0 != 2
Recap
Haaaa, so properties
are for math stuff
Common patterns
Different paths,
same result
Full circle
Constance
Execute twice
gets same
result
(Idempotence)
Hard to find,
easy to validate
Oracle
PBTs advantages
General, and thus are less brittle
Provide a better and more concise description of requirements than a bunch of examples
1 PBT can replace many, many, example-based tests
Reveal overlooked edge cases
Force you to think
Ensure deep understanding of requirements
More thinking = less typing
Force you to have a clean design
Why don’t we use
PBTs already then?
no kidding it’s really hard
• EBTs are still good for a lot of things
• Test known corner cases
• Regression
• Easier to understand for newcomers
Demo
The Diamond Kata
Given a letter, print a diamond starting with ‘A’ with the
supplied letter at the widest point.
public class Diamond
{
public static IEnumerable<string> Generate(char c)
{
yield return "A";
}
}
First NuGet
1. Non-Empty
[Property(Arbitrary = new[] { typeof(LetterGenerator) })]
public Property NotEmpty(char c)
{
return Diamond.Generate(c)
.All(s => s != string.Empty)
.ToProperty();
}
public static class LetterGenerator
{
public static Arbitrary<char> Generate() =>
Arb.Default.Char().Filter(c => c >= 'A' && c <= 'Z');
}
2. First line contains A
3. Last line contains A
[Property(Arbitrary = new[] { typeof(LetterGenerator) })]
public Property FirstLineContainsA(char c)
{
return Diamond.Generate(c)
.First().Contains('A’)
.ToProperty();
}
[Property(Arbitrary = new[] { typeof(LetterGenerator) })]
public Property LastLineContainsA(char c)
{
return Diamond.Generate(c)
.Last().Contains('A’)
.ToProperty();
}
4. Height equals Width
[Property(Arbitrary = new[] { typeof(LetterGenerator) })]
public Property DiamondWidthEqualsHeight(char c)
{
var diamond = Diamond.Generate(c).ToList();
return diamond.All(row => row.Length == diamond.Count)
.ToProperty();
}
5. Outside space
padding is
symmetric on each
side of a row
[Property(Arbitrary = new[] { typeof(LetterGenerator) })]
public Property SpacesPerRowAreSymmetric(char c)
{
return Diamond.Generate(c)
.All(row =>
CountLeadingSpaces(row) == CountTrailingSpaces(row))
.ToProperty();
}
6. Symmetric
around horizontal
axis
[Property(Arbitrary = new[] { typeof(LetterGenerator) })]
public Property SymmetricAroundHorizontalAxis(char c)
{
var diamond = Diamond.Generate(c).ToArray();
var half = diamond.Length / 2;
var topHalf = diamond[..half];
var bottomHalf = diamond[(half + 1)..];
return topHalf.Reverse()
.SequenceEqual(bottomHalf)
.ToProperty();
}
7. Symmetric
around vertical axis
[Property(Arbitrary = new[] { typeof(LetterGenerator) })]
public Property SymmetricAroundVerticalAxis(char c)
{
return Diamond.Generate(c).ToArray()
.All(row =>
{
var half = row.Length / 2;
var firstHalf = row[..half];
var secondHalf = row[(half + 1)..];
return firstHalf.Reverse().SequenceEqual(secondHalf);
}).ToProperty();
}
8. Input letter row
contains no outside
padding spaces
[Property(Arbitrary = new[] { typeof(LetterGenerator) })]
public Property InputLetterRowContainsNoOutsidePaddingSpaces
(char c)
{
var inputLetterRow = Diamond.Generate(c)
.ToArray()
.First(x => GetCharInRow(x) == c);
return (inputLetterRow[0] != ' ' && inputLetterRow[^1] != ' ')
.ToProperty();
}
First failure :’(
private static IEnumerable<string> Generate(char c)
{
yield return " A ";
yield return $"{c} {c}";
yield return " A ";
}
Works…. Most of the time
private static IEnumerable<string> Generate3(char c)
{
if (c == 'A')
{
yield return "A";
}
else
{
yield return " A ";
yield return $"{c} {c}";
yield return " A ";
}
}
Rock on!
9. Rows contain
letters in ascending
then descending
order
[Property(Arbitrary = new[] { typeof(LetterGenerator) })]
public Property RowsContainCorrectLetterInCorrectOrder
(char c)
{
var expected = new List<char>();
for (var i = 'A'; i < c; i++) expected.Add(i);
for (var i = c; i >= 'A'; i--) expected.Add(i);
var actual = Diamond.Generate(c).ToList()
.Select(GetCharInRow);
return actual.SequenceEqual(expected).ToProperty();
}
Recap
Few simple tests (10 tests, ~4-5 lines / test)
No need to change the tests when changing the implementation
Makes it easier to do TDD
Questions?
Resources
https://github.com/mbernard/property
-based-testing

Property based testing

Editor's Notes

  • #18 Trop d’exemples à construire pour bien couvrir Integration des features = multiplication des paths possible How many tests would you have to write to exhaustively prove that these two classes both work correctly under all supported configurations? “More than a human can feasibly write” is the correct answer. Now imagine having to do this for three, four, or dozens of features all working together simultaneously. You simply can’t write manual tests to cover all of the scenarios that might occur in the real world. It’s not feasible.
  • #22 Trop vite
  • #36 By generating random input, property-based tests often reveal issues that you have overlooked, such as dealing with nulls, missing data, divide by zero, negative numbers, etc. more thinking, less typing