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

Writing bulletproof code with property-based testing

Writing bulletproof code with property-based testing

Slides of my "Writing bulletproof code with property-based testing" tech talk at Droidcon Lisbon & Droidcon Berlin
-------------------------------------------------------------------------------
Unit tests are an important part of the development process. Among other things, they help us verify the correctness of our code.

Thus, when writing tests, we usually pick one or two relevant examples, let the tests run green and call it a day. This is called example-based testing.

But, is that enough? Have you missed any significant case? Have you considered all edge-cases? What if any of those non-tested examples would make the app crash?

Fear not, we can fill that gap with property-based tests. Let me show to you how, step by step.

In this tech-talk you will learn:
- some easy tips to improve your unit tests
- why example-based unit tests are not always enough
- how property-based testing can make your tests more robust
- property-based testing in very detail:
* what it is
* its pros and cons
* which tools/frameworks are available for implementing them
* how to make the most out of them

And all that by using as examples some features you would find in most Android apps, like "create account" or "text editing".
----------------------------------------------------------------
Repo with code examples:
https://github.com/sergio-sastre/Multiplying_the_quality_of_unit_tests

Sergio Sastre Flórez

April 28, 2022
Tweet

More Decks by Sergio Sastre Flórez

Other Decks in Programming

Transcript

  1. Example-based testing of your software writing examples of it tests

    specify the behaviour where by @Gio_Sastre
  2. Example-based testing @Gio_Sastre @Test
 fun is_string_palindrome() { val inputString =

    “1221”
 val revertedInput = inputString.reversed( ) assertTrue(inputString == revertedInput) 
 }
  3. Example-based testing @Gio_Sastre @Test
 fun is_string_palindrome() { val inputString =

    “1221”
 val revertedInput = inputString.reversed( ) assertTrue(inputString == revertedInput) 
 }
  4. Example-based testing @Gio_Sastre @Test
 fun is_string_palindrome() { val inputString =

    “12a21”
 val revertedInput = inputString.reversed( ) assertTrue(inputString == revertedInput) 
 }
  5. Example-based testing @Gio_Sastre @Test
 fun is_string_palindrome() { val inputString =

    “”
 val revertedInput = inputString.reversed( ) assertTrue(inputString == revertedInput) 
 }
  6. @Gio_Sastre The password contains at least 1 digit Show “must

    contain digits" otherwise 1 at least 1 latin char in upper case. Show “must contain upper case letters” otherwise 2 at least 6 chars. Show “must contain at least 6 chars” otherwise 3 no blanks. Show “must not contain blanks” otherwise 4 at least 1 latin char in lower case. Show “must contain lower case letters” otherwise 5 @Test
 fun passwordValidatorDigitsTest() { assertThat ( passwordValidator.validate(“ABCDEF”) ).contains(“must contain digits”)
 }
  7. @Gio_Sastre The password contains at least 1 digit Show “must

    contain digits" otherwise 1 at least 1 latin char in upper case. Show “must contain upper case letters” otherwise 2 at least 6 chars. Show “must contain at least 6 chars” otherwise 3 no blanks. Show “must not contain blanks” otherwise 4 at least 1 latin char in lower case. Show “must contain lower case letters” otherwise 5 @Test
 fun passwordValidatorDigitsTest() { assertThat ( passwordValidator.validate(“ABCDEF”) ).contains(“must contain digits”)
 assertThat ( passwordValidator.validate(“123456”) ).contains(“must contain upper case letters”) } @Test
 fun passwordValidatorUpperCaseTest() { }
  8. @Gio_Sastre The password contains at least 1 digit Show “must

    contain digits" otherwise 1 at least 1 latin char in upper case. Show “must contain upper case letters” otherwise 2 at least 6 chars. Show “must contain at least 6 chars” otherwise 3 no blanks. Show “must not contain blanks” otherwise 4 at least 1 latin char in lower case. Show “must contain lower case letters” otherwise 5 @ParameterizedTest(
 name = “when password \”{0}\”, error contains \”{1}\”” ) @CsvSource ( “ABCDEF, must contain digits”, “123456, must contain upper case letters” , “12345, must contain at least 6 chars”,
 “12 3 456, must not contain blanks” , “HELLO, must contain lower case letters”, )
 fun passwordValidatorTest ( password: String?, expectedError: String ? ) { assertThat ( passwordValidator.validate(password) ).contains(expectedError) }
  9. @Gio_Sastre The password contains at least 1 digit Show “must

    contain digits" otherwise 1 at least 1 latin char in upper case. Show “must contain upper case letters” otherwise 2 at least 6 chars. Show “must contain at least 6 chars” otherwise 3 no blanks. Show “must not contain blanks” otherwise 4 at least 1 latin char in lower case. Show “must contain lower case letters” otherwise 5 @ParameterizedTest(
 name = “when password \”{0}\”, error contains \”{1}\”” ) @CsvSource ( “ABCDEF, must contain digits”, “123456, must contain upper case letters” , “12345, must contain at least 6 chars”,
 “12 3 456, must not contain blanks” , “HELLO, must contain lower case letters”, )
 fun passwordValidatorTest ( password: String?, expectedError: String ? ) { assertThat ( passwordValidator.validate(password) ).contains(expectedError) }
  10. @Gio_Sastre The password contains at least 1 digit Show “must

    contain digits" otherwise 1 at least 1 latin char in upper case. Show “must contain upper case letters” otherwise 2 at least 6 chars. Show “must contain at least 6 chars” otherwise 3 no blanks. Show “must not contain blanks” otherwise 4 at least 1 latin char in lower case. Show “must contain lower case letters” otherwise 5 @ParameterizedTest(
 name = “when password \”{0}\”, error contains \”{1}\”” ) @CsvSource ( “ABCDEF, must contain digits”, “123456, must contain upper case letters” , “12345, must contain at least 6 chars”,
 “12 3 456, must not contain blanks” , “HELLO, must contain lower case letters”, )
 fun passwordValidatorTest ( password: String?, expectedError: String ? ) { assertThat ( passwordValidator.validate(password) ).contains(expectedError) }
  11. @Gio_Sastre The password contains at least 1 digit Show “must

    contain digits" otherwise 1 at least 1 latin char in upper case. Show “must contain upper case letters” otherwise 2 at least 6 chars. Show “must contain at least 6 chars” otherwise 3 no blanks. Show “must not contain blanks” otherwise 4 at least 1 latin char in lower case. Show “must contain lower case letters” otherwise 5 @ParameterizedTest(
 name = “when password \”{0}\”, error contains \”{1}\”” ) @CsvSource ( “ABCDEF, must contain digits”, “123456, must contain upper case letters” , “12345, must contain at least 6 chars”,
 “12 3 456, must not contain blanks” , “HELLO, must contain lower case letters”, )
 fun passwordValidatorTest ( password: String?, expectedError: String ? ) { assertThat ( passwordValidator.validate(password) ).contains(expectedError) }
  12. class PasswordValidator(…) { override fun isValid(password: String?): Boolean { …

    // validate contains upper case letter s if (!password?.contains(“[A-Z]”.toRegex())) { … } … } } @Gio_Sastre
  13. class PasswordValidator(…) { override fun isValid(password: String?): Boolean { …

    // validate contains upper case letter s if (!password?.contains(“[a-z]”.toRegex())) { … } … } } @Gio_Sastre
  14. Example-based testing of your software writing examples of it tests

    specify the behaviour where by @Gio_Sastre
  15. Property-based testing generating thousands of tests specify the behaviour where

    by @Gio_Sastre semi-random examples of it of your software
  16. Example-based testing @Gio_Sastre @Test
 fun is_string_palindrome() { val inputString =

    “1221”
 val revertedInput = inputString.reversed( ) assertTrue(inputString == revertedInput) 
 }
  17. fun is_string_palindrome() { val inputString = SemiRandomString(seed)
 val revertedInput =

    inputString.reversed( ) assertTrue(inputString == revertedInput) 
 } @Gio_Sastre Property-based testing @Test(tries = 1_000)
  18. fun is_string_palindrome() { val inputString = SemiRandomString(seed)
 val revertedInput =

    inputString.reversed( ) assertTrue(inputString == revertedInput) 
 } @Gio_Sastre Property-based testing @Test(tries = 1_000)
  19. @Gio_Sastre Kotlin support Nullables for non-top-level types requires @WithNull Missing

    features & configurability Previous failing seed not saved, etc. Kotlin multiplatform support Requires IntelliJ plugin to run from IDE Smooth integration Full featured & Configurable via Gradle Stateful testing, statistics, etc. Kotlin support Kotlin Native too Kotlin multiplatform support Jqwik otest
  20. @Gio_Sastre Smooth integration Full featured & Configurable via Gradle Kotlin

    support Kotlin support Stateful testing, statistics, etc. Nullables for non-top-level types requires @WithNull Missing features & configurability Previous failing seed not saved, etc. Kotlin multiplatform support Kotlin Native too Kotlin multiplatform support Requires IntelliJ plugin to run from IDE Jqwik otest
  21. @Gio_Sastre Smooth integration Full featured & Configurable via Gradle Kotlin

    support Stateful testing, statistics, etc. Nullables for non-top-level types requires @WithNull Missing features & configurability Previous failing seed not saved, etc. Kotlin multiplatform support Requires IntelliJ plugin to run from IDE Kotlin support Kotlin Native too Kotlin multiplatform support Jqwik otest
  22. @Gio_Sastre Smooth integration Full featured & Configurable via Gradle Kotlin

    support Stateful testing, statistics, etc. Nullables for non-top-level types requires @WithNull Missing features & configurability Previous failing seed not saved, etc. Kotlin multiplatform support Kotlin support Kotlin Native too Kotlin multiplatform support Requires IntelliJ plugin to run from IDE Jqwik otest
  23. @Gio_Sastre Kotlin support Nullables for non-top-level types requires @WithNull Missing

    features & configurability Previous failing seed not saved, etc. String? Smooth integration Full featured & Configurable via Gradle Stateful testing, statistics, etc. Kotlin support Kotlin Native too Kotlin multiplatform support Requires IntelliJ plugin to run from IDE Jqwik otest
  24. @Gio_Sastre Kotlin support Nullables for non-top-level types requires @WithNull Missing

    features & configurability Previous failing seed not saved, etc. List<@WithNull String> Smooth integration Full featured & Configurable via Gradle Stateful testing, statistics, etc. Kotlin support Kotlin Native too Kotlin multiplatform support Requires IntelliJ plugin to run from IDE Jqwik otest
  25. @Gio_Sastre Kotlin support Nullables for non-top-level types requires @WithNull Missing

    features & configurability Previous failing seed not saved, etc. Kotlin multiplatform support Smooth integration Full featured & Configurable via Gradle Stateful testing, statistics, etc. Kotlin support Kotlin Native too Kotlin multiplatform support Requires IntelliJ plugin to run from IDE Jqwik otest
  26. @Gio_Sastre 1 Define the property If password without upper case

    Show “must contain upper case letters” 3 Test the property Default is 1.000 times 2 Create a generator Password without upper case chars String without upper case chars
  27. @Gio_Sastre 1 Define the property If password without upper case

    Show “must contain upper case letters” fun noUpperCase() = @Provide Arbitraries.strings().ascii( ) .filter { it.matches(“[^A-Z]”.toRegex()) } 3 Test the property Default is 1.000 times 2 Create a generator Password without upper case chars String without upper case chars
  28. @Gio_Sastre 1 Define the property If password without upper case

    Show “must contain upper case letters” 2 Create a generator Password without upper case chars String without upper case chars fun noUpperCase() = @Provide Arbitraries.strings().ascii( ) .filter { it.matches(“[^A-Z]”.toRegex()) } 3 Test the property Default is 1.000 times fun testPasswordValidator ( @ForAll(“noUpperCase”) password: String ? ) { @Property val error = passwordValidator.validate(password ) assertThat(error ) .contains(“must contain upper case letters”) }
  29. @Gio_Sastre 1 Define the property If password without upper case

    Show “must contain upper case letters” 3 Test the property Default is 1.000 times fun testPasswordValidator ( @ForAll(“noUpperCase”) password: String ? ) { @Property(tries = 100) val error = passwordValidator.validate(password ) assertThat(error ) .contains(“must contain upper case letters”) } 2 Create a generator Password without upper case chars String without upper case chars fun noUpperCase() = @Provide Arbitraries.strings().ascii( ) .filter { it.matches(“[^A-Z]”.toRegex()) }
  30. @Gio_Sastre 1 Define the property If password without upper case

    Show “must contain upper case letters” 3 Test the property Default is 1.000 times fun testPasswordValidator ( @ForAll(“noUpperCase”) password: String ? ) { @Property(tries = 100) val error = passwordValidator.validate(password ) assertThat(error ) .contains(“must contain upper case letters”) } @Label(“if password without upper case, error ” + “shows ‘must contain upper case letters’”) 2 Create a generator Password without upper case chars String without upper case chars fun noUpperCase() = @Provide Arbitraries.strings().ascii( ) .filter { it.matches(“[^A-Z]”.toRegex()) }
  31. @Gio_Sastre 1 Define the property If password without upper case

    Show “must contain upper case letters” 3 Test the property Default is 1.000 times 2 Create a generator Password without upper case chars String without upper case chars fun testPasswordValidator ( @ForAll(“noUpperCase”) password: String ? ) { @Property(tries = 100) val error = passwordValidator.validate(password ) assertThat(error ) .contains(“must contain upper case letters”) } @Label(“if password without upper case, error ” + “shows ‘must contain upper case letters’”) fun noUpperCase() = @Provide Arbitraries.strings().ascii( ) .filter { it.matches(“[^A-Z]”.toRegex()) }
  32. @Gio_Sastre Shrinking Trying to find the simplest example that fails

    for the same reason “漢!字) 𡨸 $&指>事<字9§漢0=漢*字```+#会意@字<指?%” 𡨸 o字9=“ null “漢!字) 𡨸 $&指>事<字9§漢0=漢*字```+#会意@字<指?%” 𡨸 o字9=“
  33. @Gio_Sastre null “漢!字) 𡨸 $&指>事<字9§漢0=漢*字```+#会意@字<指?%” 𡨸 o字9=“ “漢!字) 𡨸 $&指>事<字9§漢0=漢*字```+#会意@字<指?%”

    𡨸 o字9=“ fun testPasswordValidator ( @ForAll(“noUpperCase”) password: String ? ) { @Property(tries = 100) val error = passwordValidator.validate(password ) assertThat(error ) .contains(“must contain upper case letters”) } @Label(“if password without upper case, error ” + “shows ‘must contain upper case letters’”) null
  34. @Gio_Sastre null “漢!字) 𡨸 $&指>事<字9§漢0=漢*字```+#会意@字<指?%” 𡨸 o字9=“ “漢!字) 𡨸 $&指>事<字9§漢0=漢*字```+#会意@字<指?%”

    𡨸 o字9=“ fun testPasswordValidator ( @ForAll(“noUpperCase”) password: String ? ) { @Property(tries = 100) val error = passwordValidator.validate(password ) assertThat(error ) .contains(“must contain upper case letters”) } @Label(“if password without upper case, error ” + “shows ‘must contain upper case letters’”) null
  35. @Gio_Sastre null “漢!字) 𡨸 $&指>事<字9§漢0=漢*字```+#会意@字<指?%” 𡨸 o字9=“ “漢!字) 𡨸 $&指>事<字9§漢0=漢*字```+#会意@字<指?%”

    𡨸 o字9=“ fun testPasswordValidator ( @ForAll(“noUpperCase”) password: String ? ) { @Property(tries = 100, seed = -3508921172508693187) val error = passwordValidator.validate(password ) assertThat(error ) .contains(“must contain upper case letters”) } @Label(“if password without upper case, error ” + “shows ‘must contain upper case letters’”) null
  36. @Gio_Sastre null “漢!字) 𡨸 $&指>事<字9§漢0=漢*字```+#会意@字<指?%” 𡨸 o字9=“ “漢!字) 𡨸 $&指>事<字9§漢0=漢*字```+#会意@字<指?%”

    𡨸 o字9=“ Total edge cases fun noUpperCase() = @Provide Arbitraries.strings().ascii( ) .filter { it.matches(“[^A-Z]”.toRegex()) } null (jqwik only)
  37. @Gio_Sastre null “漢!字) 𡨸 $&指>事<字9§漢0=漢*字```+#会意@字<指?%” 𡨸 o字9=“ “漢!字) 𡨸 $&指>事<字9§漢0=漢*字```+#会意@字<指?%”

    𡨸 o字9=“ fun noUpperCase() = @Provide Arbitraries.strings().ascii( ) .filter { it.matches(“[^A-Z]”.toRegex()) } noUpperCase().edgeCases() null (Java.lang.Character.MIN_VALUE) “/u0000” “” (Empty char) “ ” (Space char) Total edge cases (jqwik only)
  38. 1 Property-based testing main features 2 Shrinking (minimum failing sample)

    Reproduce failing tests (seeds) 3 Tests for edge cases @Gio_Sastre
  39. (if something to redo) (if something to undo) @Gio_Sastre 1

    On “text change” one more “undo” action possible, max 20 no “redo” actions possible 2 On “undo” one more “redo” action possible, max 20 next “redo” brings the previous text back 3 On “redo” one more “undo” action possible, max 20 next “undo” brings the previous text back Requirements
  40. (if something to redo) (if something to undo) @Gio_Sastre 1

    On “text change” one more “undo” action possible, max 20 no “redo” actions possible 2 On “undo” one more “redo” action possible, max 20 next “redo” brings the previous text back 3 On “redo” one more “undo” action possible, max 20 next “undo” brings the previous text back Requirements
  41. (if something to redo) @Gio_Sastre 1 On “text change” one

    more “undo” action possible, max 20 no “redo” actions possible 2 On “undo” one more “redo” action possible, max 20 next “redo” brings the previous text back 3 On “redo” one more “undo” action possible, max 20 next “undo” brings the previous text back Requirements (if something to undo)
  42. (if something to undo) @Gio_Sastre 1 On “text change” one

    more “undo” action possible, max 20 no “redo” actions possible 2 On “undo” one more “redo” action possible, max 20 next “redo” brings the previous text back 3 On “redo” one more “undo” action possible, max 20 next “undo” brings the previous text back Requirements (if something to redo)
  43. @Gio_Sastre 1 On “text change” one more “undo” action possible,

    max 20 no “redo” actions possible 2 On “undo” one more “redo” action possible, max 20 next “redo” brings the previous text back 3 On “redo” one more “undo” action possible, max 20 next “undo” brings the previous text back Requirements (if something to undo) (if something to redo)
  44. @Gio_Sastre 1 On “text change” one more “undo” action possible,

    max 20 no “redo” actions possible 2 On “undo” one more “redo” action possible, max 20 next “redo” brings the previous text back 3 On “redo” one more “undo” action possible, max 20 next “undo” brings the previous text back Requirements (if something to undo) (if something to redo)
  45. @Gio_Sastre 1 On “text change” one more “undo” action possible,

    max 20 no “redo” actions possible fun onTextChange_max_undo_actions_is_20() { @Test val editor = TextEditor( ) repeat(21){ editor.textChange(“abc”) } assertThat(editor.undoActions).size(20) } Requirements
  46. @Gio_Sastre 1 On “text change” one more “undo” action possible,

    max 20 no “redo” actions possible fun onTextChange_max_undo_actions_is_20() { @Test val editor = TextEditor( ) repeat(21){ editor.textChange(“abc”) } assertThat(editor.undoActions).size(20) } Requirements
  47. @Gio_Sastre 1 On “text change” one more “undo” action possible,

    max 20 no “redo” actions possible class TextChange(val text: String?): Action<TextEditor> { override fun run(editor: TextEditor) { val previousUndoCount = editor.undoActionsCount( ) editor.changeTextTo(text ) assertThat(editor ) .undoActionsEquals(min(previousUndoCount + 1, 20) ) .redoActionsEquals(0 ) } }
  48. @Gio_Sastre 1 On “text change” one more “undo” action possible,

    max 20 no “redo” actions possible class TextChange(val text: String?): Action<TextEditor> { override fun run(editor: TextEditor) { val previousUndoCount = editor.undoActionsCount( ) editor.changeTextTo(text ) assertThat(editor ) .undoActionsEquals(min(previousUndoCount + 1, 20) ) .redoActionsEquals(0 ) } }
  49. class TextChange(val text: String?): Action<TextEditor> { override fun run(editor: TextEditor)

    { val previousUndoCount = editor.undoActionsCount( ) editor.changeTextTo(text ) assertThat(editor ) .undoActionsEquals(min(previousUndoCount + 1, 20) ) .redoActionsEquals(0 ) } } @Gio_Sastre 1 On “text change” one more “undo” action possible, max 20 no “redo” actions possible
  50. @Gio_Sastre class TextChange(val text: String?): Action<TextEditor> { override fun run(editor:

    TextEditor) { val previousUndoCount = editor.undoActionsCount( ) editor.changeTextTo(text ) assertThat(editor ) .undoActionsEquals(min(previousUndoCount + 1, 20) ) .redoActionsEquals(0 ) } } 1 On “text change” one more “undo” action possible, max 20 no “redo” actions possible
  51. @Gio_Sastre class TextChange(val text: String?): Action<TextEditor> { override fun run(editor:

    TextEditor) { val previousUndoCount = editor.undoActionsCount( ) editor.changeTextTo(text ) assertThat(editor ) .undoActionsEquals(min(previousUndoCount + 1, 20) ) .redoActionsEquals(0 ) } } 1 On “text change” one more “undo” action possible, max 20 no “redo” actions possible
  52. @Gio_Sastre 2 On “undo” one more “redo” action possible, max

    20 next “redo” brings the previous text back (if something to undo)
  53. @Gio_Sastre 2 On “undo” one more “redo” action possible, max

    20 next “redo” brings the previous text back (if something to undo) class Undo(): Action<TextEditor> { override fun run(editor: TextEditor) { val preEditor = editor.copy( ) editor.undo( ) assertThat(editor ) .redoActionsEquals(min(preEditor.redoActions() + 1, 20) ) .nextRedoTextEquals(preEditor.text ) } } override fun precondition(editor: TextEditor) = editor.undoActionsCount() > 0
  54. @Gio_Sastre 2 On “undo” one more “redo” action possible, max

    20 next “redo” brings the previous text back (if something to undo) class Undo(): Action<TextEditor> { override fun run(editor: TextEditor) { val preEditor = editor.copy( ) editor.undo( ) assertThat(editor ) .redoActionsEquals(min(preEditor.redoActions() + 1, 20) ) .nextRedoTextEquals(preEditor.text ) } } override fun precondition(editor: TextEditor) = editor.undoActionsCount() > 0
  55. @Gio_Sastre 2 On “undo” one more “redo” action possible, max

    20 next “redo” brings the previous text back (if something to undo) class Undo(): Action<TextEditor> { override fun run(editor: TextEditor) { val preEditor = editor.copy( ) editor.undo( ) assertThat(editor ) .redoActionsEquals(min(preEditor.redoActions() + 1, 20) ) .nextRedoTextEquals(preEditor.text ) } } override fun precondition(editor: TextEditor) = editor.undoActionsCount() > 0
  56. fun randomTextChange() = Arbitraries.strings().map { TextChange(it) } fun undo() =

    Arbitraries.just(Undo()) fun redo() = Arbitraries.just(Redo()) @Gio_Sastre Semi-random action generator fun textEditorActionGenerator() = @Provide Arbitraries.sequences(randomActions()) fun randomActions() = Arbitraries.oneOf(randomTextChange(), undo(), redo())
  57. @Gio_Sastre Semi-random action generator fun textEditorActionGenerator() = @Provide Arbitraries.sequences(randomActions()) fun

    randomActions() = Arbitraries.oneOf(randomTextChange(), undo(), redo()) fun randomTextChange() = Arbitraries.strings().map { TextChange(it) } fun undo() = Arbitraries.just(Undo()) fun redo() = Arbitraries.just(Redo())
  58. @Gio_Sastre Semi-random action generator fun textEditorActionGenerator() = @Provide Arbitraries.sequences(randomActions()) fun

    randomActions() = Arbitraries.oneOf(randomTextChange(), undo(), redo()) fun randomTextChange() = Arbitraries.strings().map { TextChange(it) } fun undo() = Arbitraries.just(Undo()) fun redo() = Arbitraries.just(Redo())
  59. @Gio_Sastre Semi-random action generator fun textEditorActionGenerator() = @Provide Arbitraries.sequences(randomActions()) fun

    randomActions() = Arbitraries.oneOf(randomTextChange(), undo(), redo()) fun randomTextChange() = Arbitraries.strings().map { TextChange(it) } fun undo() = Arbitraries.just(Undo()) fun redo() = Arbitraries.just(Redo())
  60. @Gio_Sastre Semi-random action generator fun textEditorActionGenerator() = @Provide Arbitraries.sequences(randomActions()) fun

    randomActions() = Arbitraries.oneOf(randomTextChange(), undo(), redo()) fun randomTextChange() = Arbitraries.strings().map { TextChange(it) } fun undo() = Arbitraries.just(Undo()) fun redo() = Arbitraries.just(Redo())
  61. @Gio_Sastre fun randomActions() = Arbitraries.oneOf(randomTextChange(), undo(), redo()) fun textEditorActionGenerator() =

    @Provide Arbitraries.sequences(randomActions()) Property test fun statefulTextEditorTest ( @ForAll(“textEditorActionGenerator”) actions: ActionSequence<TextEditor > ) = actions.run(TextEditor()) @Property fun randomTextChange() = Arbitraries.strings().map { TextChange(it) } fun undo() = Arbitraries.just(Undo()) fun redo() = Arbitraries.just(Redo()) class TextChange(val text: String?): Action<TextEditor> { … } class Undo(): Action<TextEditor> { … } class Redo(): Action<TextEditor> { … }
  62. @Gio_Sastre class TextChange(val text: String?): Action<TextEditor> { … } class

    Undo(): Action<TextEditor> { … } class Redo(): Action<TextEditor> { … } fun statefulTextEditorTest ( @ForAll(“textEditorActionGenerator”) actions: ActionSequence<TextEditor > ) = actions.run(TextEditor()) @Property fun textEditorActionGenerator() = @Provide Arbitraries.sequences(randomActions()) Property test
  63. Cons @Gio_Sastre Pros @Gio_Sastre More robust tests Cover edge cases

    we might forget Random but deterministic tests
  64. Cons Hard to find properties Properties are not always sufficient

    @Gio_Sastre More robust tests Cover edge cases we might forget Random but deterministic tests Pros @Gio_Sastre Run more tests, so run more slowly
  65. Cons Hard to find properties @Gio_Sastre @Gio_Sastre fun String?.reversed() :

    String? { … } Create method that reverses a given string Properties are not always sufficient Run more tests, so run more slowly
  66. Cons Hard to find properties @Gio_Sastre @Gio_Sastre Patterns to find

    properties Business rule as property If password contains blanks show error message… fun String?.reversed() : String? { … } Create method that reverses a given string Properties are not always sufficient Run more tests, so run more slowly
  67. Cons @Gio_Sastre @Gio_Sastre Patterns to find properties Test Oracle list.bubbleSort()

    == list.moreEfficientSort() fun String?.reversed() : String? { … } Create method that reverses a given string Hard to find properties Properties are not always sufficient Run more tests, so run more slowly
  68. Cons @Gio_Sastre @Gio_Sastre Invariant functions Patterns to find properties string.reverse()?.length

    == string?.length fun String?.reversed() : String? { … } Create method that reverses a given string Hard to find properties Properties are not always sufficient Run more tests, so run more slowly
  69. Cons @Gio_Sastre @Gio_Sastre fun String?.reversed() : String? { … }

    @Propert y fun reversedStringKeepsLength ( @ForAll string: String ? ) { assertTrue ( string.reversed()?.length == string?.lengt h ) } Create method that reverses a given string Hard to find properties Properties are not always sufficient Run more tests, so run more slowly
  70. Cons @Gio_Sastre @Gio_Sastre Inverse functions Patterns to find properties string.reversed().reversed()

    == string @Propert y fun reversedStringKeepsLength ( @ForAll string: String ? ) { assertTrue ( string.reversed()?.length == string?.lengt h ) } fun String?.reversed() : String? { … } Create method that reverses a given string Hard to find properties Properties are not always sufficient Run more tests, so run more slowly
  71. Cons @Gio_Sastre @Gio_Sastre @Propert y fun reversedStringKeepsLength ( @ForAll string:

    String ? ) { assertTrue ( string.reversed()?.length == string?.lengt h ) } @Propert y fun reversedStringTwiceReturnsOriginal ( @ForAll string: String ? ) { assertTrue ( string.reversed().reversed() == string ) } fun String?.reversed() : String? { … } Create method that reverses a given string Hard to find properties Properties are not always sufficient Run more tests, so run more slowly
  72. Cons @Gio_Sastre @Gio_Sastre @Propert y fun reversedStringKeepsLength ( @ForAll string:

    String ? ) { assertTrue ( string.reversed()?.length == string?.lengt h ) } @Propert y fun reversedStringTwiceReturnsOriginal ( @ForAll string: String ? ) { assertTrue ( string.reversed().reversed() == string ) } fun String?.reversed() : String? { … } Create method that reverses a given string Hard to find properties Properties are not always sufficient Run more tests, so run more slowly
  73. Cons @Gio_Sastre @Gio_Sastre @Propert y fun reversedStringKeepsLength ( @ForAll string:

    String ? ) { assertTrue ( string.reversed()?.length == string?.lengt h ) } @Propert y fun reversedStringTwiceReturnsOriginal ( @ForAll string: String ? ) { assertTrue ( string.reversed().reversed() == string ) } fun String?.reversed() : String? = this Create method that reverses a given string Hard to find properties Properties are not always sufficient Run more tests, so run more slowly
  74. Cons @Gio_Sastre @Gio_Sastre @Propert y fun reversedStringKeepsLength ( @ForAll string:

    String ? ) { assertTrue ( string.reversed()?.length == string?.lengt h ) } @Propert y fun reversedStringTwiceReturnsOriginal ( @ForAll string: String ? ) { assertTrue ( string.reversed().reversed() == string ) } fun String?.reversed() : String? = this this this Create method that reverses a given string Hard to find properties Properties are not always sufficient Run more tests, so run more slowly
  75. Cons @Gio_Sastre @Gio_Sastre @Propert y fun reversedStringKeepsLength ( @ForAll string:

    String ? ) { assertTrue ( string.reversed()?.length == string?.lengt h ) } @Propert y fun reversedStringTwiceReturnsOriginal ( @ForAll string: String ? ) { assertTrue ( string.reversed().reversed() == string ) } fun String?.reversed() : String? = this this this Create method that reverses a given string Hard to find properties Properties are not always sufficient Run more tests, so run more slowly
  76. Cons @Gio_Sastre @Gio_Sastre Create method that reverses a given string

    @Propert y fun reversedStringKeepsLength ( @ForAll string: String ? ) { assertTrue ( string.reversed()?.length == string?.lengt h ) } @Propert y fun reversedStringTwiceReturnsOriginal ( @ForAll string: String ? ) { assertTrue ( string.reversed().reversed() == string ) } fun String?.reversed() : String? = this this this Hard to find properties Properties are not always sufficient Run more tests, so run more slowly
  77. Cons @Gio_Sastre @Gio_Sastre @Propert y fun reversedStringKeepsLength ( @ForAll string:

    String ? ) { assertTrue ( string.reversed()?.length == string?.lengt h ) } @Propert y fun reversedStringTwiceReturnsOriginal ( @ForAll string: String ? ) { assertTrue ( string.reversed().reversed() == string ) } fun String?.reversed() : String? { … } Create method that reverses a given string Hard to find properties Properties are not always sufficient Run more tests, so run more slowly
  78. Cons @Gio_Sastre @Gio_Sastre @Property(times = 100 ) fun reversedStringKeepsLength (

    @ForAll string: String ? ) { assertTrue ( string.reversed()?.length == string?.lengt h ) } @Property(times = 100 ) fun reversedStringTwiceReturnsOriginal ( @ForAll string: String ? ) { assertTrue ( string.reversed().reversed() == string ) } fun String?.reversed() : String? { … } Create method that reverses a given string Hard to find properties Properties are not always sufficient Run more tests, so run more slowly
  79. 1 Property-based testing (PBT) opinionated advice 2 Example + property

    tests PBT for widely used code 3 @Gio_Sastre PBT for obvious properties
  80. @Gio_Sastre code @sergio-sastre Sergio Sastre Flórez Lead & Senior Android

    developer appdev.de blogs @SergioSastre sergiosastre.hashnode.dev