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

Generating Your Assumptions

Generating Your Assumptions

Tests are critical for verifying that we’re building the software we want; however, a good test suite is hard to achieve. Setting up scenarios is boring, repetitive, and time consuming. Not to mention we have to think of all these edge cases before our users exploit them.

Property-based testing introduces developers to a new way of thinking about testing. We can create thousands of clever tests with just a small amount of code. We can then spend our time and effort effectively to help make sure the assumptions of our software hold true.

Talk Recording: https://youtu.be/cVHOhTv25AU

Michael Torres

June 29, 2019
Tweet

More Decks by Michael Torres

Other Decks in Technology

Transcript

  1. Why Test? Code confidence Prevent regressions Spend time on features

    and not on bugs Improve design of code Have happier users
  2. “Program testing can be used to show the presence of

    bugs, but never to show their absence!” - Edsger Dijkstra
  3. “I don't think that people should write a lot of

    tests. I think people should run a lot of tests” - Rich Hickey
  4. Example Properties Sorting should maintain the same size Concatenating two

    strings should contain both strings Argument order does not matter when adding numbers
  5. Many Test Cases Can generate millions of scenarios for tests

    Continuously trying to break your assumptions
  6. Frameworks Haskell - QuickCheck Java - junit-quickcheck C# - FsCheck

    Scala - ScalaCheck Kotlin - kotlintest … and many more!
  7. data class Product(val name: String, val price: Int) fun productGen():

    Gen<Product> = Gen.bind( Gen.string(), Gen.positiveIntegers()) { name, price !-> Product(name, price) } fun productsGen(): Gen<List<Product!>> = Gen.list(productGen()) Composable
  8. data class Product(val name: String, val price: Int) fun productGen():

    Gen<Product> = Gen.bind( Gen.string(), Gen.positiveIntegers()) { name, price !-> Product(name, price) } fun productsGen(): Gen<List<Product!>> = Gen.list(productGen()) Composable
  9. it("should get most recent successful order") { assertAll(ordersGen()) { orders

    !-> val result = mostRecentValidOrder(orders) } } Most Recent Valid Order
  10. Common Hurdle Don’t know what to assert against Function is

    so simple that it’s hard to find a property
  11. it("should get most recent successful order") { assertAll(ordersGen()) { orders

    !-> val result = mostRecentValidOrder(orders) !// ¯\_(ツ)_/¯ val expected = mostRecentValidOrder(orders) result.shouldBe(expected) } } Common Hurdle
  12. Modeling Test against an alternative implementation Implementation should be reliably

    correct Implementation can be a slower, naive version Implementation can also be a legacy version
  13. it("should get most recent successful order”) { assertAll(ordersGen()) { orders

    !-> val result = mostRecentValidOrder(orders) val expected = orders.filter { order !-> order.isSuccess() } .sortedByDescending { order !-> order.date } .firstOrNull() result.shouldBe(expected) } } Most Recent Valid Order
  14. it("should get most recent successful order”) { assertAll(ordersGen()) { orders

    !-> val result = mostRecentValidOrder(orders) val expected = orders.filter { order !-> order.isSuccess() } .sortedByDescending { order !-> order.date } .firstOrNull() result.shouldBe(expected) } } Most Recent Valid Order
  15. it("should always get the last id") { getLastAddedProductId(listOf(1)).shouldBe(1) getLastAddedProductId(listOf(1, 2,

    3)).shouldBe(3) getLastAddedProductId(listOf(5, 4)).shouldBe(4) } Get Last Product Id
  16. it("should always get the last id") { val idGen =

    Gen.positiveIntegers() val idsGen = Gen.list(idGen) assertAll(idsGen, idGen) { productIds, id !-> val allIds = productIds.plus(id) getLastAddedProductId(allIds).shouldBe(id) } } Get Last Product Id
  17. it("should always get the last id") { val idGen =

    Gen.positiveIntegers() val idsGen = Gen.list(idGen) assertAll(idsGen, idGen) { productIds, id !-> val allIds = productIds.plus(id) getLastAddedProductId(allIds).shouldBe(id) } } Get Last Product Id
  18. Invariants Breaking down into smaller properties 
 “Strong ropes are

    built from 
 smaller threads put together”
  19. it("the total should be at least zero") { assertAll(productsGen(), discountCodeGen())

    { products, discount !-> val total = calculateTotal(products, discount) total.shouldBeGreaterThanOrEqual(0) } } Checkout Total
  20. it("the total should be at least zero") { assertAll(productsGen(), discountCodeGen())

    { products, discount !-> val total = calculateTotal(products, discount) total.shouldBeGreaterThanOrEqual(0) } } Checkout Total
  21. it("zero items should always have a total of zero") {

    assertAll(discountCodeGen()) { discount !-> val total = calculateTotal(emptyList(), discount) total.shouldBe(0) } } Checkout Total
  22. it("zero items should always have a total of zero") {

    assertAll(discountCodeGen()) { discount !-> val total = calculateTotal(emptyList(), discount) total.shouldBe(0) } } Checkout Total
  23. it("a valid discount code should always reduce the total") {

    assertAll( nonEmptyProductsGen(), validDiscountCodeGen()) { products, discount !-> val totalWithoutDiscount = calculateTotal(products, null) val totalWithDiscount = calculateTotal(products, discount) totalWithDiscount.shouldBeLessThan(totalWithoutDiscount) } } Checkout Total
  24. it("a valid discount code should always reduce the total") {

    assertAll( nonEmptyProductsGen(), validDiscountCodeGen()) { products, discount !-> val totalWithoutDiscount = calculateTotal(products, null) val totalWithDiscount = calculateTotal(products, discount) totalWithDiscount.shouldBeLessThan(totalWithoutDiscount) } } Checkout Total
  25. it("serialized orders should be deserialzed to original") { assertAll(ordersGen()) {

    orders !-> val result = deserialize(serialize(orders)) result.shouldContainExactly(orders) } } JSON Parsing
  26. it("serialized orders should be deserialzed to original") { assertAll(ordersGen()) {

    orders !-> val result = deserialize(serialize(orders)) result.shouldContainExactly(orders) } } JSON Parsing
  27. it("serialized orders should be deserialzed to original") { assertAll(ordersGen()) {

    orders !-> val serialized = serialize(orders) assertIsJson(serialized) val deserialized = deserialize(serialized) deserialized.shouldContainExactly(orders) } } JSON Parsing
  28. Data Generators generate data Data can be more than just

    for inputs Data can represent actions
  29. fun save(key: Key, value: Value)
 fun remove(key: Key) fun get(key:

    Key): Value? fun getAll(): Set<Value> fun clear() fun size(): Int Database
  30. fun put(key: Key, value: Value)
 fun remove(key: Key) fun get(key:

    Key): Value? fun values(): MutableCollection<Value> fun clear() fun size(): Int HashMap
  31. sealed class Action<Key, Value> { class Save<Key, Value>( val key:

    Key, val value: Value) : Action<Key, Value>() class Remove<Key, Value>(val key: Key) : Action<Key, Value>() class Get<Key, Value>(val key: Key) : Action<Key, Value>() class Clear<Key, Value> : Action<Key, Value>() class Size<Key, Value> : Action<Key, Value>() } Actions
  32. fun <Key, Value> actionGen( keyGen: Gen<Key>, valueGen: Gen<Value>): Gen<Action<Key, Value!>>

    = Gen.oneOf( Gen.bind(keyGen, valueGen) { key, value !-> Action.Save(key, value) }, keyGen.map { key !-> Action.Remove(key) }, keyGen.map { key !-> Action.Get(key) }, Gen.create { Action.GetAll() }, Gen.create { Action.Clear() }, Gen.create { Action.Size() } ) Generator
  33. fun <Key, Value> actionGen( keyGen: Gen<Key>, valueGen: Gen<Value>): Gen<Action<Key, Value!>>

    = Gen.oneOf( Gen.bind(keyGen, valueGen) { key, value !-> Action.Save(key, value) }, keyGen.map { key !-> Action.Remove(key) }, keyGen.map { key !-> Action.Get(key) }, Gen.create { Action.GetAll() }, Gen.create { Action.Clear() }, Gen.create { Action.Size() } ) Generator
  34. fun <Key, Value> actionGen( keyGen: Gen<Key>, valueGen: Gen<Value>): Gen<Action<Key, Value!>>

    = Gen.oneOf( Gen.bind(keyGen, valueGen) { key, value !-> Action.Save(key, value) }, keyGen.map { key !-> Action.Remove(key) }, keyGen.map { key !-> Action.Get(key) }, Gen.create { Action.GetAll() }, Gen.create { Action.Clear() }, Gen.create { Action.Size() } ) Generator
  35. fun <Key, Value> actionGen( keyGen: Gen<Key>, valueGen: Gen<Value>): Gen<Action<Key, Value!>>

    = Gen.oneOf( Gen.bind(keyGen, valueGen) { key, value !-> Action.Save(key, value) }, keyGen.map { key !-> Action.Remove(key) }, keyGen.map { key !-> Action.Get(key) }, Gen.create { Action.GetAll() }, Gen.create { Action.Clear() }, Gen.create { Action.Size() } ) Generator
  36. fun <Key, Value> actionGen( keyGen: Gen<Key>, valueGen: Gen<Value>): Gen<Action<Key, Value!>>

    = Gen.oneOf( Gen.bind(keyGen, valueGen) { key, value !-> Action.Save(key, value) }, keyGen.map { key !-> Action.Remove(key) }, keyGen.map { key !-> Action.Get(key) }, Gen.create { Action.GetAll() }, Gen.create { Action.Clear() }, Gen.create { Action.Size() } ) Generator
  37. private fun <Key, Value> runAction( model: MutableMap<Key, Value>, action: Action<Key,

    Value>) { when (action) { is Action.Save !-> model.put(action.key, action.value) is Action.Remove !-> model.remove(action.key) is Action.Get !-> model.get(action.key) is Action.GetAll !-> model.values is Action.Clear !-> model.clear() is Action.Size !-> model.size } } Runner - Model
  38. private fun <Key, Value> runAction( model: MutableMap<Key, Value>, action: Action<Key,

    Value>) { when (action) { is Action.Save !-> model.put(action.key, action.value) is Action.Remove !-> model.remove(action.key) is Action.Get !-> model.get(action.key) is Action.GetAll !-> model.values is Action.Clear !-> model.clear() is Action.Size !-> model.size } } Runner - Model
  39. private fun <Key, Value> runAction( db: DB, action: Action<Key, Value>)

    { when (action) { is Action.Save !-> db.save(action.key, action.value) is Action.Remove !-> db.remove(action.key) is Action.Get !-> db.get(action.key) is Action.GetAll !-> db.getAll() is Action.Clear !-> db.clear() is Action.Size !-> db.size() } } Runner - Database
  40. private fun <Key, Value> runAction( db: DB, action: Action<Key, Value>)

    { when (action) { is Action.Save !-> db.save(action.key, action.value) is Action.Remove !-> db.remove(action.key) is Action.Get !-> db.get(action.key) is Action.GetAll !-> db.getAll() is Action.Clear !-> db.clear() is Action.Size !-> db.size() } } Runner - Database
  41. describe(“saving orders in key value database") { val keyGen =

    Gen.choose(0, 50) val actionsGen = Gen.list(actionGen(keyGen, ordersGen())) it("should have same values as a map") { assertAll(actionsGen) { actions !-> val model = HashMap<Int, Order>() val db = DB() actions.forEach { action !-> runAction(model, action) runAction(db, action) } db.getAll().shouldContainExactlyInAnyOrder(model.values) } } } Test
  42. describe(“saving orders in key value database") { val keyGen =

    Gen.choose(0, 50) val actionsGen = Gen.list(actionGen(keyGen, ordersGen())) it("should have same values as a map") { assertAll(actionsGen) { actions !-> val model = HashMap<Int, Order>() val db = DB() actions.forEach { action !-> runAction(model, action) runAction(db, action) } db.getAll().shouldContainExactlyInAnyOrder(model.values) } } } Test
  43. describe(“saving orders in key value database") { val keyGen =

    Gen.choose(0, 50) val actionsGen = Gen.list(actionGen(keyGen, ordersGen())) it("should have same values as a map") { assertAll(actionsGen) { actions !-> val model = HashMap<Int, Order>() val db = DB() actions.forEach { action !-> runAction(model, action) runAction(db, action) } db.getAll().shouldContainExactlyInAnyOrder(model.values) } } } Test
  44. No Test Unhappy user found it “Doesn’t work. 1 star,

    0 if I could” Check analytics or crash reports?
  45. Example Test Assertion fails
 Collection should be exactly [Order(id=2, date=2019-06-24,

    products=[Product(name=, price=2)])] but was [Order(id=1, date=2019-06-24, products=[Product(name=, price=1)])] Debug and figure out why
  46. Property Test Assertion fails
 Collection should be exactly [Order(id=2, date=2019-06-24,

    products=[Product(name=, price=2)])] but was [Order(id=1, date=2019-06-24, products=[Product(name=, price=1)])] See steps from shrinking
 Shrink result !=> [ Save(key=1, value=Order(id=1, date=2019-06-24, products=[Product(name="", price=1)], Remove(key=1), Save(key=2, value=Order(id=2, date=2019-06-24, products=[Product(name="", price=2)] ] fail
  47. Perfect Bug Report Exact methods to run In this order

    With the simplest inputs Shrink result !=> [ Save(key=1, value=Order(id=1, date=2019-06-24, products=[Product(name="", price=1)], Remove(key=1), Save(key=2, value=Order(id=2, date=2019-06-24, products=[Product(name="", price=2)] ] fail
  48. Example Tests Known edge cases Faster to run Not everything

    can be expressed as a property Easier to write
  49. Clojure Bug in core found when converting between immutable and

    mutable data structures. Fixed in Clojure 1.6 Clojure spec provides out of the box support for property based testing Functional Lisp for the JVM
  50. LevelDB Bug found using the model pattern within a few

    minutes Required 17 steps to reproduce Involved opening and closing the database, adding and deleting the same key Key-Value database from Google
  51. js-yaml Over 1 million downloads per day Bug found using

    symmetry pattern in ~10 lines of code YAML parsing JavaScript library
  52. Learn More Property-Based Testing with PropEr, Erlang, and Elixir
 https://propertesting.com/

    Don’t Write Tests - John Hughes
 https://www.youtube.com/watch?v=hXnS_Xjwk2Y Generating test cases so you don’t have to
 https://labs.spotify.com/2015/06/25/rapid-check/