SlideShare a Scribd company logo
An introduction to property-
based testing
Vincent Pradeilles - Worldline
Classic unit tests
“Given, When, Then”
Classic unit tests
Given some state

When I perform this action

Then I expect that result
Classic unit tests
func testForValidData() {
// Given
let validData = MyData.valid()
// When
let result = myBusinessLogic(data)
// Then
XCTAssert(/* assert that result meets expectations */)
}
Classic unit tests
func testForInvalidData() {
// Given
let invalidData = MyData.invalid()
// When
let result = myBusinessLogic(invalidData)
// Then
XCTAssert(/* assert that result handles error */)
}
Valid data
Invalid data
Valid data
Invalid data
Valid data
Invalid data
Edge cases
Classic unit tests
👍 Great at catching regressions

👎 Don’t allow to learn anything new
Could tests discover edge cases ?
🤔
Data Properties
Properties Data
Let’s look at an example
Let’s look at an example
extension Array {
public func reversed() -> Array<Element>
}
How do we define a property?
Using natural language
“If an array is reversed twice, then the result
is equal to the input”
Using natural language
“If an array is reversed twice, then the result
is equal to the input”

“Any array reversed twice is equal to itself”
Using first order logic
∀ a ∈ Array, reverse(reverse(a)) = a
Using functional programming
func checkArrayReverse(_ array: Array) -> Bool {
return array.reversed().reversed() == array
}
Using functional programming
func checkArrayReverse(_ array: Array) -> Bool {
return array.reversed().reversed() == array
}
∀ arr ∈ Array, reverse(reverse(arr)) = arr
Using functional programming
func checkArrayReverse(_ array: Array) -> Bool {
return array.reversed().reversed() == array
}
∀ arr ∈ Array, reverse(reverse(arr)) = arr
Using functional programming
func checkArrayReverse(_ array: Array) -> Bool {
return array.reversed().reversed() == array
}
∀ arr ∈ Array, reverse(reverse(arr)) = arr
Using functional programming
func checkArrayReverse(_ array: Array) -> Bool {
return array.reversed().reversed() == array
}
∀ arr ∈ Array, reverse(reverse(arr)) = arr
Property-based testing in a
nutshell
Property-based testing in a nutshell
For any input values (x,y, z, …)
Such that precondition (x, y, z, …) is satisfied
Property (x, y, z, …) must be true
Property-based testing in a nutshell
For any input value (array)
Property (array.reversed().reversed == array)
must be true
How do we implement such
tests?
Introducing SwiftCheck
SwiftCheck
SwiftCheck is a framework that let us write and run property-based tests.

It works by following a simple set of rules:

A. Test the property using random input values

B. If the property fails, shrink the responsible input until the property no
longer fails

C. Return either OK or the smallest counterexample
SwiftCheck
public protocol Arbitrary {
public static var arbitrary: Gen<Self> { get }
public static func shrink(_: Self) -> [Self]
}
Implementing a first property
property("Simple test of array reversal") <- forAll({ (array: [Int]) -> Testable in
return array.reversed().reversed() == array
})
Now let’s define a more
complex property 💪
A more complex property
We want to test that the elements of the array are
correctly reversed.

For any input values (array, i)
Such that array.isEmpty == false, 0 ≤ i < (array.count - 1)
Property (array.reversed()[i] == array[(array.count - 1) - i])
must be true
A more complex property
property("Elements are correctly reversed") <- forAll({ (array: [Int]) -> Testable in
return (!array.isEmpty ==> {
let arrayIndices = Gen.fromElements(of: array.indices)
return forAll(arrayIndices) { (index: Int) -> Testable in
return array.reversed()[index] == array[(array.count - 1) - index]
}
})
})
A more complex property
property("Elements are correctly reversed") <- forAll({ (array: [Int]) -> Testable in
return (!array.isEmpty ==> {
let arrayIndices = Gen.fromElements(of: array.indices)
return forAll(arrayIndices) { (index: Int) -> Testable in
return array.reversed()[index] == array[(array.count - 1) - index]
}
})
})
A more complex property
property("Elements are correctly reversed") <- forAll({ (array: [Int]) -> Testable in
return (!array.isEmpty ==> {
let arrayIndices = Gen.fromElements(of: array.indices)
return forAll(arrayIndices) { (index: Int) -> Testable in
return array.reversed()[index] == array[(array.count - 1) - index]
}
})
})
A more complex property
property("Elements are correctly reversed") <- forAll({ (array: [Int]) -> Testable in
return (!array.isEmpty ==> {
let arrayIndices = Gen.fromElements(of: array.indices)
return forAll(arrayIndices) { (index: Int) -> Testable in
return array.reversed()[index] == array[(array.count - 1) - index]
}
})
})
A more complex property
property("Elements are correctly reversed") <- forAll({ (array: [Int]) -> Testable in
return (!array.isEmpty ==> {
let arrayIndices = Gen.fromElements(of: array.indices)
return forAll(arrayIndices) { (index: Int) -> Testable in
return array.reversed()[index] == array[(array.count - 1) - index]
}
})
})
A more complex property
property("Elements are correctly reversed") <- forAll({ (array: [Int]) -> Testable in
return (!array.isEmpty ==> {
let arrayIndices = Gen.fromElements(of: array.indices)
return forAll(arrayIndices) { (index: Int) -> Testable in
return array.reversed()[index] == array[(array.count - 1) - index]
}
})
})
Time to catch a bug 🐛
Time to catch a bug 🐛
Requirement: we need to validate email addresses.

We asked Google, and got this nice regex:

let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,64}"
How could we test this regex?
Time to catch a bug 🐛
func isValidEmail(_ candidateEmail: String) -> Bool {
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,64}"
let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
return emailTest.evaluate(with: candidateEmail)
}
Create a generator for emails
let upper: Gen<Character> = Gen<Character>.fromElements(in: "A"..."Z")
let lower: Gen<Character> = Gen<Character>.fromElements(in: "a"..."z")
let numeric: Gen<Character> = Gen<Character>.fromElements(in: "0"..."9")
let special: Gen<Character> = Gen<Character>.fromElements(of: ["!", "#", "$", "%", "&",
"'", "*", "+", "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~", "."])
let localEmail = Gen<Character>.one(of: [
upper,
lower,
numeric,
special,
]).proliferateNonEmpty
.suchThat({ $0[($0.endIndex - 1)] != "." })
.map(String.init(_:))
Create a generator for emails
let upper: Gen<Character> = Gen<Character>.fromElements(in: "A"..."Z")
let lower: Gen<Character> = Gen<Character>.fromElements(in: "a"..."z")
let numeric: Gen<Character> = Gen<Character>.fromElements(in: "0"..."9")
let special: Gen<Character> = Gen<Character>.fromElements(of: ["!", "#", "$", "%", "&",
"'", "*", "+", "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~", "."])
let localEmail = Gen<Character>.one(of: [
upper,
lower,
numeric,
special,
]).proliferateNonEmpty
.suchThat({ $0[($0.endIndex - 1)] != "." })
.map(String.init(_:))
Create a generator for emails
let upper: Gen<Character> = Gen<Character>.fromElements(in: "A"..."Z")
let lower: Gen<Character> = Gen<Character>.fromElements(in: "a"..."z")
let numeric: Gen<Character> = Gen<Character>.fromElements(in: "0"..."9")
let special: Gen<Character> = Gen<Character>.fromElements(of: ["!", "#", "$", "%", "&",
"'", "*", "+", "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~", "."])
let localEmail = Gen<Character>.one(of: [
upper,
lower,
numeric,
special,
]).proliferateNonEmpty
.suchThat({ $0[($0.endIndex - 1)] != "." })
.map(String.init(_:))
Create a generator for emails
let upper: Gen<Character> = Gen<Character>.fromElements(in: "A"..."Z")
let lower: Gen<Character> = Gen<Character>.fromElements(in: "a"..."z")
let numeric: Gen<Character> = Gen<Character>.fromElements(in: "0"..."9")
let special: Gen<Character> = Gen<Character>.fromElements(of: ["!", "#", "$", "%", "&",
"'", "*", "+", "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~", "."])
let localEmail = Gen<Character>.one(of: [
upper,
lower,
numeric,
special,
]).proliferateNonEmpty
.suchThat({ $0[($0.endIndex - 1)] != "." })
.map(String.init(_:))
Create a generator for emails
let upper: Gen<Character> = Gen<Character>.fromElements(in: "A"..."Z")
let lower: Gen<Character> = Gen<Character>.fromElements(in: "a"..."z")
let numeric: Gen<Character> = Gen<Character>.fromElements(in: "0"..."9")
let special: Gen<Character> = Gen<Character>.fromElements(of: ["!", "#", "$", "%", "&",
"'", "*", "+", "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~", "."])
let localEmail = Gen<Character>.one(of: [
upper,
lower,
numeric,
special,
]).proliferateNonEmpty
.suchThat({ $0[($0.endIndex - 1)] != "." })
.map(String.init(_:))
Create a generator for emails
let upper: Gen<Character> = Gen<Character>.fromElements(in: "A"..."Z")
let lower: Gen<Character> = Gen<Character>.fromElements(in: "a"..."z")
let numeric: Gen<Character> = Gen<Character>.fromElements(in: "0"..."9")
let special: Gen<Character> = Gen<Character>.fromElements(of: ["!", "#", "$", "%", "&",
"'", "*", "+", "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~", "."])
let localEmail = Gen<Character>.one(of: [
upper,
lower,
numeric,
special,
]).proliferateNonEmpty
.suchThat({ $0[($0.endIndex - 1)] != "." })
.map(String.init(_:))
Write the property
let emailGen = glue([localEmail, Gen.pure("@"), hostname, Gen.pure("."), tld])
let args = CheckerArguments(maxTestCaseSize: 10)
property("Email passes validation", arguments: args) <- forAll(emailGen) { (email: String) in
return isValidEmail(email)
}.noShrinking
Write the property
let emailGen = glue([localEmail, Gen.pure("@"), hostname, Gen.pure("."), tld])
let args = CheckerArguments(maxTestCaseSize: 10)
property("Email passes validation", arguments: args) <- forAll(emailGen) { (email: String) in
return isValidEmail(email)
}.noShrinking
Write the property
let emailGen = glue([localEmail, Gen.pure("@"), hostname, Gen.pure("."), tld])
let args = CheckerArguments(maxTestCaseSize: 10)
property("Email passes validation", arguments: args) <- forAll(emailGen) { (email: String) in
return isValidEmail(email)
}.noShrinking
Write the property
let emailGen = glue([localEmail, Gen.pure("@"), hostname, Gen.pure("."), tld])
let args = CheckerArguments(maxTestCaseSize: 10)
property("Email passes validation", arguments: args) <- forAll(emailGen) { (email: String) in
return isValidEmail(email)
}.noShrinking
Write the property
let emailGen = glue([localEmail, Gen.pure("@"), hostname, Gen.pure("."), tld])
let args = CheckerArguments(maxTestCaseSize: 10)
property("Email passes validation", arguments: args) <- forAll(emailGen) { (email: String) in
return isValidEmail(email)
}.noShrinking
*** Failed! Proposition: Email passes validation
Falsifiable (after 2 tests):
|@o.az
Write the property
*** Failed! Proposition: Email passes validation
Falsifiable (after 2 tests):
|@o.az
We managed to catch a bug!
🐛
Can we test for invalid e-mails?
Can we test for invalid e-mails?
It’s actually impossible 😭 Why ?
Generating invalid e-mails would require a precondition to tell valid and
invalid e-mails appart…

…which is exactly what we need to test!

In such a case, good old data based testing is the only option '
Mixing both approaches
Mixing both approaches
extension Array where Element == Double {
func average() -> Double
}
Mixing both approaches
// Data based testing
XCTAssertEqual([1, 5, 10, 50].average(), 16.5)
Mixing both approaches
// Data based testing
XCTAssertEqual([1, 5, 10, 50].average(), 16.5)
// Property based testing
property("average of products equals product of average") <- forAllNoShrink(genScalar,
genValues) { (a: Double, x: [Double]) in
let averageOfProducts = (a * x).average()
let productOfAverages = a * x.average()
return areEqual(averageOfProducts, productOfAverages)
}
Mixing both approaches
// Data based testing
XCTAssertEqual([1, 5, 10, 50].average(), 16.5)
// Property based testing
property("average of products equals product of average") <- forAllNoShrink(genScalar,
genValues) { (a: Double, x: [Double]) in
let averageOfProducts = (a * x).average()
let productOfAverages = a * x.average()
return areEqual(averageOfProducts, productOfAverages)
}
property("average of sums equals sum of averages") <- forAllNoShrink(genValues,
genValues) { (x: [Double], y: [Double]) in
makeSameSize(&x, &y)
let averageOfSums = (x + y).average()
let sumOfAverages = x.average() + y.average()
return areEqual(averageOfSums, sumOfAverages)
}
How do I use this in my app? 🤔
How do I use this in my app?
Properties are “easy” to define for the lower levels of an app.

But as we get closer to business requirements, things get messy pretty fast.

However, consider the following property:

property("Trivial") <- forAll({ (input: Input) -> Testable in
myBusinessLogic(input)
return true
})
How do I use this in my app?
This property might – rightfully – seem trivial.

However, it actually performs two useful assertions:

• The function “myBusinessLogic” does return

• The function “myBusinessLogic” doesn’t crash

Pretty useful, for something so trivial 👌

You could even have a code-generation tool write it for you )
How do I use this in my app?
“Trivial” properties also make sense in other contexts!

Consider UI testing: in most cases, it makes sense to test that views don’t overlap.

That’s quite easy to implement using property-based testing:

• Generate a random model

• Use it to fill up your view

• Iterate over its subviews and check that no two have frames that overlap

(Thank you to Pierre Felgines for this cool use case!)
When is this approach relevant?
When is this approach relevant?
• External data (user input, web services, etc.)

• Encoding / Decoding

• Regular expressions

• Custom sorting algorithm

• Timezone and Date arithmetic

• SDK development
Is there a lighter approach?
Is there a lighter approach?
let faker = Faker(locale: "fr_FR")
let fakeName = faker.name.firstName()
let fakeText = faker.lorem.sentence()
Is there a lighter approach?
let faker = Faker(locale: "fr_FR")
let fakeName = faker.name.firstName()
let fakeText = faker.lorem.sentence()
extension XCTestCase {
func fuzz(_ times: Int = 100, test: () throws -> Void) rethrows {
for _ in 1...times {
try test()
}
}
}
Is there a lighter approach?
class TestCase: XCTestCase {
func test() {
fuzz {
// the test gets run 100 times
}
}
}
An introduction to property-based testing
An introduction to property-based testing
😊

More Related Content

PDF
TDC218SP | Trilha Kotlin - DSLs in a Kotlin Way
PPTX
Javascript And J Query
PDF
Lucene
PDF
Developer Testing Tools Roundup
PDF
Scala in practice
PDF
Clean code with google guava jee conf
PDF
Google guava
PDF
The core libraries you always wanted - Google Guava
TDC218SP | Trilha Kotlin - DSLs in a Kotlin Way
Javascript And J Query
Lucene
Developer Testing Tools Roundup
Scala in practice
Clean code with google guava jee conf
Google guava
The core libraries you always wanted - Google Guava

What's hot (17)

PDF
Google Guava for cleaner code
PDF
Jquery 13 cheatsheet_v1
PDF
Jquery 1.3 cheatsheet_v1
PDF
Google Guava
PDF
How te bring common UI patterns to ADF
PDF
Chaining and function composition with lodash / underscore
PDF
Pragmatic Real-World Scala (short version)
PDF
Google guava overview
PPTX
Akka patterns
PDF
Tweaking the interactive grid
PDF
Lodash js
PDF
JavaScript Fundamentals with Angular and Lodash
PPTX
Working with Groovy Collections
PPTX
How to Bring Common UI Patterns to ADF
PDF
Nik Graf - Get started with Reason and ReasonReact
PDF
Selenium cheat sheet
PDF
Unbreakable: The Craft of Code
Google Guava for cleaner code
Jquery 13 cheatsheet_v1
Jquery 1.3 cheatsheet_v1
Google Guava
How te bring common UI patterns to ADF
Chaining and function composition with lodash / underscore
Pragmatic Real-World Scala (short version)
Google guava overview
Akka patterns
Tweaking the interactive grid
Lodash js
JavaScript Fundamentals with Angular and Lodash
Working with Groovy Collections
How to Bring Common UI Patterns to ADF
Nik Graf - Get started with Reason and ReasonReact
Selenium cheat sheet
Unbreakable: The Craft of Code
Ad

Similar to An introduction to property-based testing (20)

PDF
ppopoff
PDF
Meet scala
PDF
Elm: give it a try
PPT
SDC - Einführung in Scala
PDF
Introduction to Scala
PDF
Pragmatic Real-World Scala
PDF
pragmaticrealworldscalajfokus2009-1233251076441384-2.pdf
PDF
pragmaticrealworldscalajfokus2009-1233251076441384-2.pdf
PDF
pragmaticrealworldscalajfokus2009-1233251076441384-2.pdf
PDF
pragmaticrealworldscalajfokus2009-1233251076441384-2.pdf
PDF
Functional Programming with Groovy
PDF
Useful javascript
PDF
Kotlin Basics - Apalon Kotlin Sprint Part 2
PDF
A Prelude of Purity: Scaling Back ZIO
PDF
Ruslan Shevchenko - Property based testing
KEY
Introduction à CoffeeScript pour ParisRB
PDF
Everything About PowerShell
PDF
(How) can we benefit from adopting scala?
PDF
Scala coated JVM
PDF
Beyond xUnit example-based testing: property-based testing with ScalaCheck
ppopoff
Meet scala
Elm: give it a try
SDC - Einführung in Scala
Introduction to Scala
Pragmatic Real-World Scala
pragmaticrealworldscalajfokus2009-1233251076441384-2.pdf
pragmaticrealworldscalajfokus2009-1233251076441384-2.pdf
pragmaticrealworldscalajfokus2009-1233251076441384-2.pdf
pragmaticrealworldscalajfokus2009-1233251076441384-2.pdf
Functional Programming with Groovy
Useful javascript
Kotlin Basics - Apalon Kotlin Sprint Part 2
A Prelude of Purity: Scaling Back ZIO
Ruslan Shevchenko - Property based testing
Introduction à CoffeeScript pour ParisRB
Everything About PowerShell
(How) can we benefit from adopting scala?
Scala coated JVM
Beyond xUnit example-based testing: property-based testing with ScalaCheck
Ad

More from Vincent Pradeilles (10)

PDF
On-Boarding New Engineers
PDF
The underestimated power of KeyPaths
PDF
Solving callback hell with good old function composition
PDF
Taking the boilerplate out of your tests with Sourcery
PDF
How to build a debug view almost for free
PDF
Implementing pseudo-keywords through Functional Programing
PDF
Property Wrappers or how Swift decided to become Java
PDF
Cocoa heads 09112017
PDF
Advanced functional programing in Swift
PDF
Monads in Swift
On-Boarding New Engineers
The underestimated power of KeyPaths
Solving callback hell with good old function composition
Taking the boilerplate out of your tests with Sourcery
How to build a debug view almost for free
Implementing pseudo-keywords through Functional Programing
Property Wrappers or how Swift decided to become Java
Cocoa heads 09112017
Advanced functional programing in Swift
Monads in Swift

Recently uploaded (20)

PDF
Digital Strategies for Manufacturing Companies
PDF
wealthsignaloriginal-com-DS-text-... (1).pdf
PDF
Design an Analysis of Algorithms II-SECS-1021-03
PDF
Which alternative to Crystal Reports is best for small or large businesses.pdf
PDF
Nekopoi APK 2025 free lastest update
PPTX
VVF-Customer-Presentation2025-Ver1.9.pptx
PPTX
Agentic AI : A Practical Guide. Undersating, Implementing and Scaling Autono...
PDF
How Creative Agencies Leverage Project Management Software.pdf
PDF
Softaken Excel to vCard Converter Software.pdf
PDF
Addressing The Cult of Project Management Tools-Why Disconnected Work is Hold...
PDF
medical staffing services at VALiNTRY
PDF
Wondershare Filmora 15 Crack With Activation Key [2025
PDF
Raksha Bandhan Grocery Pricing Trends in India 2025.pdf
PDF
EN-Survey-Report-SAP-LeanIX-EA-Insights-2025.pdf
PPTX
Odoo POS Development Services by CandidRoot Solutions
PDF
2025 Textile ERP Trends: SAP, Odoo & Oracle
PPTX
Reimagine Home Health with the Power of Agentic AI​
PPTX
L1 - Introduction to python Backend.pptx
PPTX
Oracle E-Business Suite: A Comprehensive Guide for Modern Enterprises
PDF
Internet Downloader Manager (IDM) Crack 6.42 Build 41
Digital Strategies for Manufacturing Companies
wealthsignaloriginal-com-DS-text-... (1).pdf
Design an Analysis of Algorithms II-SECS-1021-03
Which alternative to Crystal Reports is best for small or large businesses.pdf
Nekopoi APK 2025 free lastest update
VVF-Customer-Presentation2025-Ver1.9.pptx
Agentic AI : A Practical Guide. Undersating, Implementing and Scaling Autono...
How Creative Agencies Leverage Project Management Software.pdf
Softaken Excel to vCard Converter Software.pdf
Addressing The Cult of Project Management Tools-Why Disconnected Work is Hold...
medical staffing services at VALiNTRY
Wondershare Filmora 15 Crack With Activation Key [2025
Raksha Bandhan Grocery Pricing Trends in India 2025.pdf
EN-Survey-Report-SAP-LeanIX-EA-Insights-2025.pdf
Odoo POS Development Services by CandidRoot Solutions
2025 Textile ERP Trends: SAP, Odoo & Oracle
Reimagine Home Health with the Power of Agentic AI​
L1 - Introduction to python Backend.pptx
Oracle E-Business Suite: A Comprehensive Guide for Modern Enterprises
Internet Downloader Manager (IDM) Crack 6.42 Build 41

An introduction to property-based testing

  • 1. An introduction to property- based testing Vincent Pradeilles - Worldline
  • 3. Classic unit tests Given some state When I perform this action Then I expect that result
  • 4. Classic unit tests func testForValidData() { // Given let validData = MyData.valid() // When let result = myBusinessLogic(data) // Then XCTAssert(/* assert that result meets expectations */) }
  • 5. Classic unit tests func testForInvalidData() { // Given let invalidData = MyData.invalid() // When let result = myBusinessLogic(invalidData) // Then XCTAssert(/* assert that result handles error */) }
  • 9. Classic unit tests 👍 Great at catching regressions 👎 Don’t allow to learn anything new
  • 10. Could tests discover edge cases ? 🤔
  • 13. Let’s look at an example
  • 14. Let’s look at an example extension Array { public func reversed() -> Array<Element> }
  • 15. How do we define a property?
  • 16. Using natural language “If an array is reversed twice, then the result is equal to the input”
  • 17. Using natural language “If an array is reversed twice, then the result is equal to the input” “Any array reversed twice is equal to itself”
  • 18. Using first order logic ∀ a ∈ Array, reverse(reverse(a)) = a
  • 19. Using functional programming func checkArrayReverse(_ array: Array) -> Bool { return array.reversed().reversed() == array }
  • 20. Using functional programming func checkArrayReverse(_ array: Array) -> Bool { return array.reversed().reversed() == array } ∀ arr ∈ Array, reverse(reverse(arr)) = arr
  • 21. Using functional programming func checkArrayReverse(_ array: Array) -> Bool { return array.reversed().reversed() == array } ∀ arr ∈ Array, reverse(reverse(arr)) = arr
  • 22. Using functional programming func checkArrayReverse(_ array: Array) -> Bool { return array.reversed().reversed() == array } ∀ arr ∈ Array, reverse(reverse(arr)) = arr
  • 23. Using functional programming func checkArrayReverse(_ array: Array) -> Bool { return array.reversed().reversed() == array } ∀ arr ∈ Array, reverse(reverse(arr)) = arr
  • 25. Property-based testing in a nutshell For any input values (x,y, z, …) Such that precondition (x, y, z, …) is satisfied Property (x, y, z, …) must be true
  • 26. Property-based testing in a nutshell For any input value (array) Property (array.reversed().reversed == array) must be true
  • 27. How do we implement such tests?
  • 29. SwiftCheck SwiftCheck is a framework that let us write and run property-based tests. It works by following a simple set of rules: A. Test the property using random input values B. If the property fails, shrink the responsible input until the property no longer fails C. Return either OK or the smallest counterexample
  • 30. SwiftCheck public protocol Arbitrary { public static var arbitrary: Gen<Self> { get } public static func shrink(_: Self) -> [Self] }
  • 31. Implementing a first property property("Simple test of array reversal") <- forAll({ (array: [Int]) -> Testable in return array.reversed().reversed() == array })
  • 32. Now let’s define a more complex property 💪
  • 33. A more complex property We want to test that the elements of the array are correctly reversed. For any input values (array, i) Such that array.isEmpty == false, 0 ≤ i < (array.count - 1) Property (array.reversed()[i] == array[(array.count - 1) - i]) must be true
  • 34. A more complex property property("Elements are correctly reversed") <- forAll({ (array: [Int]) -> Testable in return (!array.isEmpty ==> { let arrayIndices = Gen.fromElements(of: array.indices) return forAll(arrayIndices) { (index: Int) -> Testable in return array.reversed()[index] == array[(array.count - 1) - index] } }) })
  • 35. A more complex property property("Elements are correctly reversed") <- forAll({ (array: [Int]) -> Testable in return (!array.isEmpty ==> { let arrayIndices = Gen.fromElements(of: array.indices) return forAll(arrayIndices) { (index: Int) -> Testable in return array.reversed()[index] == array[(array.count - 1) - index] } }) })
  • 36. A more complex property property("Elements are correctly reversed") <- forAll({ (array: [Int]) -> Testable in return (!array.isEmpty ==> { let arrayIndices = Gen.fromElements(of: array.indices) return forAll(arrayIndices) { (index: Int) -> Testable in return array.reversed()[index] == array[(array.count - 1) - index] } }) })
  • 37. A more complex property property("Elements are correctly reversed") <- forAll({ (array: [Int]) -> Testable in return (!array.isEmpty ==> { let arrayIndices = Gen.fromElements(of: array.indices) return forAll(arrayIndices) { (index: Int) -> Testable in return array.reversed()[index] == array[(array.count - 1) - index] } }) })
  • 38. A more complex property property("Elements are correctly reversed") <- forAll({ (array: [Int]) -> Testable in return (!array.isEmpty ==> { let arrayIndices = Gen.fromElements(of: array.indices) return forAll(arrayIndices) { (index: Int) -> Testable in return array.reversed()[index] == array[(array.count - 1) - index] } }) })
  • 39. A more complex property property("Elements are correctly reversed") <- forAll({ (array: [Int]) -> Testable in return (!array.isEmpty ==> { let arrayIndices = Gen.fromElements(of: array.indices) return forAll(arrayIndices) { (index: Int) -> Testable in return array.reversed()[index] == array[(array.count - 1) - index] } }) })
  • 40. Time to catch a bug 🐛
  • 41. Time to catch a bug 🐛 Requirement: we need to validate email addresses. We asked Google, and got this nice regex: let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,64}" How could we test this regex?
  • 42. Time to catch a bug 🐛 func isValidEmail(_ candidateEmail: String) -> Bool { let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,64}" let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailTest.evaluate(with: candidateEmail) }
  • 43. Create a generator for emails let upper: Gen<Character> = Gen<Character>.fromElements(in: "A"..."Z") let lower: Gen<Character> = Gen<Character>.fromElements(in: "a"..."z") let numeric: Gen<Character> = Gen<Character>.fromElements(in: "0"..."9") let special: Gen<Character> = Gen<Character>.fromElements(of: ["!", "#", "$", "%", "&", "'", "*", "+", "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~", "."]) let localEmail = Gen<Character>.one(of: [ upper, lower, numeric, special, ]).proliferateNonEmpty .suchThat({ $0[($0.endIndex - 1)] != "." }) .map(String.init(_:))
  • 44. Create a generator for emails let upper: Gen<Character> = Gen<Character>.fromElements(in: "A"..."Z") let lower: Gen<Character> = Gen<Character>.fromElements(in: "a"..."z") let numeric: Gen<Character> = Gen<Character>.fromElements(in: "0"..."9") let special: Gen<Character> = Gen<Character>.fromElements(of: ["!", "#", "$", "%", "&", "'", "*", "+", "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~", "."]) let localEmail = Gen<Character>.one(of: [ upper, lower, numeric, special, ]).proliferateNonEmpty .suchThat({ $0[($0.endIndex - 1)] != "." }) .map(String.init(_:))
  • 45. Create a generator for emails let upper: Gen<Character> = Gen<Character>.fromElements(in: "A"..."Z") let lower: Gen<Character> = Gen<Character>.fromElements(in: "a"..."z") let numeric: Gen<Character> = Gen<Character>.fromElements(in: "0"..."9") let special: Gen<Character> = Gen<Character>.fromElements(of: ["!", "#", "$", "%", "&", "'", "*", "+", "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~", "."]) let localEmail = Gen<Character>.one(of: [ upper, lower, numeric, special, ]).proliferateNonEmpty .suchThat({ $0[($0.endIndex - 1)] != "." }) .map(String.init(_:))
  • 46. Create a generator for emails let upper: Gen<Character> = Gen<Character>.fromElements(in: "A"..."Z") let lower: Gen<Character> = Gen<Character>.fromElements(in: "a"..."z") let numeric: Gen<Character> = Gen<Character>.fromElements(in: "0"..."9") let special: Gen<Character> = Gen<Character>.fromElements(of: ["!", "#", "$", "%", "&", "'", "*", "+", "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~", "."]) let localEmail = Gen<Character>.one(of: [ upper, lower, numeric, special, ]).proliferateNonEmpty .suchThat({ $0[($0.endIndex - 1)] != "." }) .map(String.init(_:))
  • 47. Create a generator for emails let upper: Gen<Character> = Gen<Character>.fromElements(in: "A"..."Z") let lower: Gen<Character> = Gen<Character>.fromElements(in: "a"..."z") let numeric: Gen<Character> = Gen<Character>.fromElements(in: "0"..."9") let special: Gen<Character> = Gen<Character>.fromElements(of: ["!", "#", "$", "%", "&", "'", "*", "+", "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~", "."]) let localEmail = Gen<Character>.one(of: [ upper, lower, numeric, special, ]).proliferateNonEmpty .suchThat({ $0[($0.endIndex - 1)] != "." }) .map(String.init(_:))
  • 48. Create a generator for emails let upper: Gen<Character> = Gen<Character>.fromElements(in: "A"..."Z") let lower: Gen<Character> = Gen<Character>.fromElements(in: "a"..."z") let numeric: Gen<Character> = Gen<Character>.fromElements(in: "0"..."9") let special: Gen<Character> = Gen<Character>.fromElements(of: ["!", "#", "$", "%", "&", "'", "*", "+", "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~", "."]) let localEmail = Gen<Character>.one(of: [ upper, lower, numeric, special, ]).proliferateNonEmpty .suchThat({ $0[($0.endIndex - 1)] != "." }) .map(String.init(_:))
  • 49. Write the property let emailGen = glue([localEmail, Gen.pure("@"), hostname, Gen.pure("."), tld]) let args = CheckerArguments(maxTestCaseSize: 10) property("Email passes validation", arguments: args) <- forAll(emailGen) { (email: String) in return isValidEmail(email) }.noShrinking
  • 50. Write the property let emailGen = glue([localEmail, Gen.pure("@"), hostname, Gen.pure("."), tld]) let args = CheckerArguments(maxTestCaseSize: 10) property("Email passes validation", arguments: args) <- forAll(emailGen) { (email: String) in return isValidEmail(email) }.noShrinking
  • 51. Write the property let emailGen = glue([localEmail, Gen.pure("@"), hostname, Gen.pure("."), tld]) let args = CheckerArguments(maxTestCaseSize: 10) property("Email passes validation", arguments: args) <- forAll(emailGen) { (email: String) in return isValidEmail(email) }.noShrinking
  • 52. Write the property let emailGen = glue([localEmail, Gen.pure("@"), hostname, Gen.pure("."), tld]) let args = CheckerArguments(maxTestCaseSize: 10) property("Email passes validation", arguments: args) <- forAll(emailGen) { (email: String) in return isValidEmail(email) }.noShrinking
  • 53. Write the property let emailGen = glue([localEmail, Gen.pure("@"), hostname, Gen.pure("."), tld]) let args = CheckerArguments(maxTestCaseSize: 10) property("Email passes validation", arguments: args) <- forAll(emailGen) { (email: String) in return isValidEmail(email) }.noShrinking *** Failed! Proposition: Email passes validation Falsifiable (after 2 tests): |@o.az
  • 54. Write the property *** Failed! Proposition: Email passes validation Falsifiable (after 2 tests): |@o.az
  • 55. We managed to catch a bug! 🐛
  • 56. Can we test for invalid e-mails?
  • 57. Can we test for invalid e-mails? It’s actually impossible 😭 Why ? Generating invalid e-mails would require a precondition to tell valid and invalid e-mails appart… …which is exactly what we need to test! In such a case, good old data based testing is the only option '
  • 59. Mixing both approaches extension Array where Element == Double { func average() -> Double }
  • 60. Mixing both approaches // Data based testing XCTAssertEqual([1, 5, 10, 50].average(), 16.5)
  • 61. Mixing both approaches // Data based testing XCTAssertEqual([1, 5, 10, 50].average(), 16.5) // Property based testing property("average of products equals product of average") <- forAllNoShrink(genScalar, genValues) { (a: Double, x: [Double]) in let averageOfProducts = (a * x).average() let productOfAverages = a * x.average() return areEqual(averageOfProducts, productOfAverages) }
  • 62. Mixing both approaches // Data based testing XCTAssertEqual([1, 5, 10, 50].average(), 16.5) // Property based testing property("average of products equals product of average") <- forAllNoShrink(genScalar, genValues) { (a: Double, x: [Double]) in let averageOfProducts = (a * x).average() let productOfAverages = a * x.average() return areEqual(averageOfProducts, productOfAverages) } property("average of sums equals sum of averages") <- forAllNoShrink(genValues, genValues) { (x: [Double], y: [Double]) in makeSameSize(&x, &y) let averageOfSums = (x + y).average() let sumOfAverages = x.average() + y.average() return areEqual(averageOfSums, sumOfAverages) }
  • 63. How do I use this in my app? 🤔
  • 64. How do I use this in my app? Properties are “easy” to define for the lower levels of an app. But as we get closer to business requirements, things get messy pretty fast. However, consider the following property: property("Trivial") <- forAll({ (input: Input) -> Testable in myBusinessLogic(input) return true })
  • 65. How do I use this in my app? This property might – rightfully – seem trivial. However, it actually performs two useful assertions: • The function “myBusinessLogic” does return • The function “myBusinessLogic” doesn’t crash Pretty useful, for something so trivial 👌 You could even have a code-generation tool write it for you )
  • 66. How do I use this in my app? “Trivial” properties also make sense in other contexts! Consider UI testing: in most cases, it makes sense to test that views don’t overlap. That’s quite easy to implement using property-based testing: • Generate a random model • Use it to fill up your view • Iterate over its subviews and check that no two have frames that overlap (Thank you to Pierre Felgines for this cool use case!)
  • 67. When is this approach relevant?
  • 68. When is this approach relevant? • External data (user input, web services, etc.) • Encoding / Decoding • Regular expressions • Custom sorting algorithm • Timezone and Date arithmetic • SDK development
  • 69. Is there a lighter approach?
  • 70. Is there a lighter approach? let faker = Faker(locale: "fr_FR") let fakeName = faker.name.firstName() let fakeText = faker.lorem.sentence()
  • 71. Is there a lighter approach? let faker = Faker(locale: "fr_FR") let fakeName = faker.name.firstName() let fakeText = faker.lorem.sentence() extension XCTestCase { func fuzz(_ times: Int = 100, test: () throws -> Void) rethrows { for _ in 1...times { try test() } } }
  • 72. Is there a lighter approach? class TestCase: XCTestCase { func test() { fuzz { // the test gets run 100 times } } }
  • 75. 😊