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

Types Vs Tests

Types Vs Tests

How Types and Typed FP affects the way we test programs

https://github.com/47deg/types-vs-tests

Raúl Raja Martínez

June 05, 2018
Tweet

More Decks by Raúl Raja Martínez

Other Decks in Programming

Transcript

  1. Better Types !" Fewer Tests
    (@raulraja , @47deg) !" Sources, Slides 1

    View full-size slide

  2. Who am I?
    @raulraja
    @47deg
    • Co-Founder and CTO at 47 Degrees
    • Scala advisory board member
    • FP advocate
    • Electric Guitar @
    (@raulraja , @47deg) !" Sources, Slides 2

    View full-size slide

  3. More tests !" Better Software?
    (@raulraja , @47deg) !" Sources, Slides 3

    View full-size slide

  4. What are we testing?
    (@raulraja , @47deg) !" Sources, Slides 4

    View full-size slide

  5. Testing : Programs
    Programs
    class Counter(var amount: Int) {
    require(amount !" 0, s"($amount seed value) must be a positive integer")
    def increase(): Unit =
    amount !# 1
    }
    (@raulraja , @47deg) !" Sources, Slides 5

    View full-size slide

  6. What are we testing? !" Input values
    class CounterSpec extends BaseTest {
    test("Can't be constructed with negative numbers") {
    the [IllegalArgumentException] thrownBy {
    new Counter(-1)
    } should have message "requirement failed: (-1 seed value) must be a positive integer"
    }
    }
    !" defined class CounterSpec
    (new CounterSpec).execute
    !" CounterSpec:
    !" - Can't be constructed with negative numbers
    (@raulraja , @47deg) !" Sources, Slides 6

    View full-size slide

  7. What are we testing? !" Side effects
    class CounterSpec extends BaseTest {
    test("`Counter#amount` is mutated after `Counter#increase` is invoked") {
    val counter = new Counter(0)
    counter.increase()
    counter.amount shouldBe 1
    }
    }
    !" defined class CounterSpec
    (new CounterSpec).execute
    !" CounterSpec:
    !" - `Counter#amount` is mutated after `Counter#increase` is invoked
    (@raulraja , @47deg) !" Sources, Slides 7

    View full-size slide

  8. What are we testing? !" Output values
    class CounterSpec extends BaseTest {
    test("`Counter#amount` is properly initialized") {
    new Counter(0).amount shouldBe 0
    }
    }
    !" defined class CounterSpec
    (new CounterSpec).execute
    !" CounterSpec:
    !" - `Counter#amount` is properly initialized
    (@raulraja , @47deg) !" Sources, Slides 8

    View full-size slide

  9. What are we testing? !" Runtime
    Our component is now distributed and may fail
    import scala.concurrent._
    import cats.data.NonEmptyList
    import scala.concurrent.ExecutionContext.Implicits.global
    import java.util.concurrent.atomic.AtomicInteger
    import scala.util.control._
    sealed abstract class KnownError extends Throwable with NoStackTrace
    case object ServiceUnavailable extends KnownError
    case class CounterOutOfRange(msg: NonEmptyList[String]) extends KnownError
    class FutureCounter(val amount: AtomicInteger) {
    require(amount.get !" 0, s"($amount seed value) must be a positive atomic integer")
    def increase(): Future[Either[KnownError, Int!# =
    Future(Right(amount.incrementAndGet)) !$ mocked for demo purposes
    }
    (@raulraja , @47deg) !" Sources, Slides 9

    View full-size slide

  10. What are we testing? !" Runtime
    Changes in requirements
    class FutureCounterSpec extends BaseTest {
    test("`FutureCounter#amount` is mutated after `FutureCounter#increase` is invoked") {
    val counter = new FutureCounter(new AtomicInteger(0))
    counter.increase()
    counter.amount.get shouldBe 1
    }
    }
    !" defined class FutureCounterSpec
    (new FutureCounterSpec).execute
    !" FutureCounterSpec:
    !" - `FutureCounter#amount` is mutated after `FutureCounter#increase` is invoked !!# FAILED !!#
    !" 0 was not equal to 1 (:26)
    (@raulraja , @47deg) !" Sources, Slides 10

    View full-size slide

  11. What are we testing? !" Runtime
    Changes in requirements
    import scala.concurrent.duration._
    !" import scala.concurrent.duration._
    class FutureCounterSpec extends BaseTest {
    test("`FutureCounter#amount` is mutated after `FutureCounter#increase` is invoked") {
    val counter = new FutureCounter(new AtomicInteger(0))
    val result = counter.increase() map { _ !# counter.amount.get shouldBe 1 }
    Await.result(result, 10.seconds)
    }
    }
    !" defined class FutureCounterSpec
    (new FutureCounterSpec).execute
    !" FutureCounterSpec:
    !" - `FutureCounter#amount` is mutated after `FutureCounter#increase` is invoked
    (@raulraja , @47deg) !" Sources, Slides 11

    View full-size slide

  12. What are we testing?
    • Input values are in range of acceptance
    (-N is not)
    • Side effects caused by programs
    (counter is mutated in the ou9er scope)
    • Programs produce expected output values given correct input values.
    (counter value is consistent with our biz logic)
    • Run3me machinery
    (The program may work sync/async and it may fail)
    (@raulraja , @47deg) !" Sources, Slides 12

    View full-size slide

  13. What are we NOT testing?
    (@raulraja , @47deg) !" Sources, Slides 13

    View full-size slide

  14. We don't test for: Invariants
    (@raulraja , @47deg) !" Sources, Slides 14

    View full-size slide

  15. We don't test for: Invariants
    In computer science, an invariant is a condi2on that can be relied
    upon to be true during execu2on of a program
    ― Wikipedia Invariant(computerscience)
    (@raulraja , @47deg) !" Sources, Slides 15

    View full-size slide

  16. We don't test for: Invariants
    In computer science, an invariant is a condi2on that can be relied
    upon to be true during execu2on of a program
    • Compila)on: We trust the compiler says our values will be
    constrained by proper)es
    • Math Laws: (iden)ty, associa)vity, commuta)vity, ...)
    • 3rd party dependencies
    (@raulraja , @47deg) !" Sources, Slides 16

    View full-size slide

  17. The Dark Path
    Now, ask yourself why these defects happen too o3en.
    If your answer is that our languages don’t prevent them,
    then I strongly suggest that you quit your job and never
    think about being a programmer again;
    because defects are never the fault of our languages.
    Defects are the fault of programmers.
    It is programmers who create defects – not languages.
    ― Robert C. Mar-n (Uncle Bob) The Dark Path
    (@raulraja , @47deg) !" Sources, Slides 17

    View full-size slide

  18. The Dark Path
    And what is it that programmers are supposed to do to prevent
    defects?
    I’ll give you one guess. Here are some hints.
    It’s a verb. It starts with a “T”. Yeah.
    You got it. TEST!
    ― Robert C. Mar-n (Uncle Bob) The Dark Path
    (@raulraja , @47deg) !" Sources, Slides 18

    View full-size slide

  19. The Dark Path
    And what is it that programmers are supposed to do to prevent
    defects?
    I’ll give you one guess. Here are some hints.
    It’s a verb. It starts with a “T”. Yeah.
    You got it. TEST!
    ― Robert C. Mar-n (Uncle Bob) The Dark Path
    (@raulraja , @47deg) !" Sources, Slides 19

    View full-size slide

  20. Does our programming style affect the way we test?
    (@raulraja , @47deg) !" Sources, Slides 20

    View full-size slide

  21. What is Functional Programming?
    In computer science, func0onal programming
    is a programming paradigm.
    A style of building the structure and elements
    of computer programs that treats computa0on
    as the evalua0on of mathema0cal func0ons
    and avoids changing-state and mutable data.
    -- Wikipedia
    (@raulraja , @47deg) !" Sources, Slides 21

    View full-size slide

  22. Common traits of Functional Programming
    • Higher-order func0ons
    • Immutable data
    • Referen0al transparency
    • Lazy evalua0on
    • Recursion
    • Abstrac0ons
    (@raulraja , @47deg) !" Sources, Slides 22

    View full-size slide

  23. What are we testing?
    Back to our original concerns
    • Input values are in range of acceptance
    • Programs produce an expected output value given an accepted
    input value.
    • Side effects caused by programs
    • Changes in requirements
    (@raulraja , @47deg) !" Sources, Slides 23

    View full-size slide

  24. What are we testing? !" Input values
    counter: Int is a poorly chosen type. Let's fix that!
    class CounterSpec extends BaseTest {
    test("Can't be constructed with negative numbers") {
    the [IllegalArgumentException] thrownBy {
    new Counter(-1)
    } should have message "requirement failed: (-1 seed value) must be a positive integer"
    }
    }
    !" defined class CounterSpec
    (new CounterSpec).execute
    !" CounterSpec:
    !" - Can't be constructed with negative numbers
    (@raulraja , @47deg) !" Sources, Slides 24

    View full-size slide

  25. What are we testing? !" Input values
    Refining Int constrains our values at compile and run4me
    import eu.timepit.refined.W
    import eu.timepit.refined.cats.syntax._
    import eu.timepit.refined.api.{Refined, RefinedTypeOps}
    import eu.timepit.refined.numeric._
    type Zero = W.`0`.T
    type Ten = W.`10`.T
    type Amount = Int Refined Interval.Closed[Zero, Ten]
    object Amount extends RefinedTypeOps[Amount, Int]
    class Counter(var amount: Amount) {
    def increase(): Unit =
    Amount.from(amount.value + 1).foreach(v !" amount = v)
    }
    (@raulraja , @47deg) !" Sources, Slides 25

    View full-size slide

  26. What are we testing? !" Input values
    The compiler can verify the range and we can properly type amount
    + import eu.timepit.refined.api.{Refined, RefinedTypeOps}
    + import eu.timepit.refined.numeric._
    + type Zero = W.`0`.T
    + type Ten = W.`10`.T
    + type Amount = Int Refined Interval.Closed[Zero, Ten]
    + object Amount extends RefinedTypeOps[Amount, Int]
    - class Counter(var amount: Int) {
    + class Counter(var amount: Amount) {
    - require(amount !" 0, s"($amount seed value) must be a positive integer")
    def increase(): Unit =
    - amount !# 1
    + Amount.from(amount.value + 1).foreach(v !$ amount = v)
    }
    (@raulraja , @47deg) !" Sources, Slides 26

    View full-size slide

  27. What are we testing? !" Input values
    We can s(ll test this but this test proves nothing
    import eu.timepit.refined.scalacheck.numeric._
    !" import eu.timepit.refined.scalacheck.numeric._
    class CounterSpec extends BaseTest {
    test("`Amount` values are within range") {
    check((amount: Amount) !# amount.value !$ 0 !% amount.value !& 10)
    }
    }
    !" defined class CounterSpec
    (new CounterSpec).execute
    !" CounterSpec:
    !" - `Amount` values are within range
    (@raulraja , @47deg) !" Sources, Slides 27

    View full-size slide

  28. What are we testing? !" Input values
    The compiler can verify the range and we can properly type amount
    class CounterSpec extends BaseTest {
    - test("Can't be constructed with negative numbers") {
    - the [IllegalArgumentException] thrownBy {
    - new Counter(-1)
    - } should have message "requirement failed: (-1 seed value) must be a positive integer"
    - }
    }
    (@raulraja , @47deg) !" Sources, Slides 28

    View full-size slide

  29. What are we testing?
    Back to our original concerns
    • Input values are in range of acceptance
    • Side effects caused by programs
    • Programs produce an expected output value given an accepted
    input value.
    • Changes in requirements
    (@raulraja , @47deg) !" Sources, Slides 29

    View full-size slide

  30. What are we testing? !" Side effects
    class CounterSpec extends BaseTest {
    test("`Counter#amount` is mutated after `Counter#increase` is invoked") {
    val counter = new Counter(Amount(0))
    counter.increase()
    counter.amount.value shouldBe 1
    }
    }
    !" defined class CounterSpec
    (new CounterSpec).execute
    !" CounterSpec:
    !" - `Counter#amount` is mutated after `Counter#increase` is invoked
    (@raulraja , @47deg) !" Sources, Slides 30

    View full-size slide

  31. What are we testing? !" Side effects
    class Counter(var amount: Amount) { !" mutable
    def increase(): Unit = !" Unit does not return anything useful
    Amount.from(amount.value + 1).foreach(v !# amount = v) !" mutates the external scope
    }
    (@raulraja , @47deg) !" Sources, Slides 31

    View full-size slide

  32. What are we testing? !" Side effects
    No need to test side effects if func.ons are PURE!
    class Counter(val amount: Amount) { !" values are immutable
    def increase(): Either[KnownError, Counter] = !" Every operation returns an immutable copy
    Amount.validate(amount.value + 1).fold( !" Amount.validate does not need to be tested
    { errors !# Left(CounterOutOfRange(errors)) }, !" No Exceptions are thrown
    { a !# Right(new Counter(a)) }
    )
    }
    (@raulraja , @47deg) !" Sources, Slides 32

    View full-size slide

  33. What are we testing? !" Side effects
    Side effects caused by programs.
    - class Counter(var amount: Amount) { !" mutable
    + class Counter(val amount: Amount) { !" values are immutable
    - def increase(): Unit = !" Unit does not return anything useful
    + def increase(): Counter = !" Every operation returns an immutable copy
    - Amount.from(amount.value + 1).foreach(v !# amount = v) !" mutates the external scope
    + Amount.validate(amount.value + 1).fold( !" Amount.validate does not need to be tested
    + { errors !# Left(CounterOutOfRange(errors)) }, !" No Exceptions are thrown
    + { a !# Right(new Counter(a)) }
    + )
    }
    (@raulraja , @47deg) !" Sources, Slides 33

    View full-size slide

  34. What are we testing?
    Back to our original concerns
    • Input values are in range of acceptance
    • Side effects caused by programs
    • Programs produce an expected output value given an accepted
    input value
    • Changes in requirements
    (@raulraja , @47deg) !" Sources, Slides 34

    View full-size slide

  35. What are we testing? !" Output values
    Programs produce an expected output value given an accepted input
    class CounterSpec extends BaseTest {
    test("`Counter#amount` is immutable and pure") {
    new Counter(Amount(0)).increase().map(_.amount) shouldBe Right(Amount(1))
    }
    }
    !" defined class CounterSpec
    (new CounterSpec).execute
    !" CounterSpec:
    !" - `Counter#amount` is immutable and pure
    (@raulraja , @47deg) !" Sources, Slides 35

    View full-size slide

  36. What are we testing?
    Back to our original concerns
    • Input values are in range of acceptance
    • Side effects caused by programs
    • Programs produce an expected output value given an accepted
    input value
    • Run$me requirements
    (@raulraja , @47deg) !" Sources, Slides 36

    View full-size slide

  37. What are we testing? !" Runtime
    Changes in run,me requirements force us to consider other effects
    (async, failures,...)
    class FutureCounter(val amount: Amount) { !" values are immutable
    def increase(): Future[Either[KnownError, Counter!# = !" Every operation returns an immutable copy
    Future {
    Amount.validate(amount.value + 1).fold( !" Amount.validate does not need to be tested
    { error !$ Left(CounterOutOfRange(error)) }, !" potential failures are also contemplated
    { a !$ Right(new Counter(a)) }
    )
    }
    }
    (@raulraja , @47deg) !" Sources, Slides 37

    View full-size slide

  38. What are we testing? !" Runtime
    We are forcing call sites to block even those that did not want to be
    async
    class CounterSpec extends BaseTest {
    test("`FutureCounter#amount` is immutable and pure") {
    val asyncResult = new FutureCounter(Amount(0)).increase()
    Await.result(asyncResult, 10.seconds).map(_.amount) shouldBe Right(Amount(1))
    }
    }
    !" defined class CounterSpec
    (new CounterSpec).execute
    !" CounterSpec:
    !" - `FutureCounter#amount` is immutable and pure
    (@raulraja , @47deg) !" Sources, Slides 38

    View full-size slide

  39. What are we testing? !" Runtime
    Concrete data types lack flexibility and increase the chance of bugs
    class FutureCounter(val amount: Amount) {
    def increase: Future[Either[KnownError, Counter!" =
    Future {
    Amount.validate(amount.value + 1).fold(
    { error !# Left(CounterOutOfRange(error)) },
    { a !# Right(new Counter(a)) }
    )
    }
    }
    (@raulraja , @47deg) !" Sources, Slides 39

    View full-size slide

  40. What are we testing? !" Runtime
    Everything we need to describe computa6on
    Type class Combinator
    Functor map, lift
    Applicative pure, ap
    ApplicativeError raiseError, catch
    Monad flatMap, flatten
    MonadError ensure, rethrow
    Sync delay, suspend
    Async async
    Effect toIO
    (@raulraja , @47deg) !" Sources, Slides 40

    View full-size slide

  41. What are we testing? !" Runtime
    Everything we need to describe combina4on
    Type class Combinator
    Semigroup combine
    Monoid empty
    Foldable foldLeft, foldRight
    Traverse traverse, sequence
    (@raulraja , @47deg) !" Sources, Slides 41

    View full-size slide

  42. What are we testing? !" Runtime
    Abstract type classes increase flexibility and decrease the chance
    of bugs
    import cats.effect.Sync
    !" F[_] can be any box for which `Sync` instance is available
    class Counter[F[_!#(val amount: Amount)(implicit F: Sync[F]) {
    !" A counter is returned in a generic box
    def increase(): F[Counter[F!# = {
    F.suspend { !" Effects in the block are deferred
    Amount.validate(amount.value + 1).fold(
    { error !$ F.raiseError(CounterOutOfRange(error)) },
    { a !$ F.pure(new Counter(a)) }
    )
    }
    }
    }
    (@raulraja , @47deg) !" Sources, Slides 42

    View full-size slide

  43. What are we testing? !" Runtime
    Our tests may also be polymorphic
    import cats.effect._
    import cats.effect.implicits._
    class CounterSpec[F[_!"(implicit F: Effect[F]) extends BaseTest {
    test("`Counter#amount` is immutable and pure") {
    val result: F[Counter[F!" = new Counter[F](Amount(0)).increase()
    F.toIO(result).unsafeRunSync().amount shouldBe Amount(1)
    }
    }
    (@raulraja , @47deg) !" Sources, Slides 43

    View full-size slide

  44. What are we testing? !" Runtime
    A la carte
    (new CounterSpec[IO]).execute
    !" CounterSpec:
    !" - `Counter#amount` is immutable and pure
    (@raulraja , @47deg) !" Sources, Slides 44

    View full-size slide

  45. What are we testing? !" Runtime
    A la carte
    import monix.eval.Task
    !" import monix.eval.Task
    import monix.execution.Scheduler.Implicits.global
    !" import monix.execution.Scheduler.Implicits.global
    (new CounterSpec[Task]).execute
    !" CounterSpec:
    !" - `Counter#amount` is immutable and pure
    (@raulraja , @47deg) !" Sources, Slides 45

    View full-size slide

  46. What are we testing? !" Runtime
    A la carte (only for the well behaved)
    (new CounterSpec[Future]).execute !" fails to compile, because Future can't suspend effects
    !" :46: error: Cannot find implicit value for Effect[scala.concurrent.Future].
    !" Building this implicit value might depend on having an implicit
    !" s.c.ExecutionContext in scope, a Scheduler or some equivalent type.
    !" (new CounterSpec[Future]).execute !" fails to compile, because Future can't suspend effects
    !" ^
    (@raulraja , @47deg) !" Sources, Slides 46

    View full-size slide

  47. What are we testing?
    Back to our original concerns
    • Input values are in range of acceptance
    • Side effects caused by programs
    • Programs produce an expected output value given an accepted
    input value
    • Run(@raulraja , @47deg) !" Sources, Slides 47

    View full-size slide