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
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
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 (<console>:26) (@raulraja , @47deg) !" Sources, Slides 10
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
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
is a condi2on that can be relied upon to be true during execu2on of a program ― Wikipedia Invariant(computerscience) (@raulraja , @47deg) !" Sources, Slides 15
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
for the well behaved) (new CounterSpec[Future]).execute !" fails to compile, because Future can't suspend effects !" <console>: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
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 47