Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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