Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions E4-PropertyBasedTests
File renamed without changes.
Empty file added HeartRate/.projectile
Empty file.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
version = "2.0.0"
style = defaultWithAlign
maxColumn = 120
project.git = true
Expand Down
File renamed without changes.
26 changes: 26 additions & 0 deletions HeartRate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# What If We exclusively use property based testing

## Proposition

* Property based testing is an effective way of documenting contracts (pre and
post conditions to functions), or other laws / properties in your codebase.
* Property based testing facilitates the generation of specific test cases and
thus speeds up the speed with which you write tests.
* Property based testing reduces duplication, but providing a framework for
re-use of specific data values.
* Property based testing increases the coverage of your code and thus helps
detect harder to find bugs.

## Experiment Videos

### From unit to property
Convert an existing unit test, to a property based test.

### From property to unit.
Use the generator framework to run a specific test.

### Refine the input types.
Use type refinement to ensure the compiler catches illegal values instead of relying on unit testing.

### Refine dependent input.
Use type refinements and path dependent types to ensure the compiler catches illegal values instead of relying on unit testing.
10 changes: 5 additions & 5 deletions propertybasedtesting/build.sbt → HeartRate/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ lazy val root = (project in file("."))
"-Ywarn-unused:imports",
"-Xfatal-warnings"
))))
// .enablePlugins(ScalafmtPlugin)
.enablePlugins(ScalafmtPlugin)

lazy val projectSettings = Seq(
name := "PropertyBasedTesting",
name := "HeartRateSample",
version := "0.1.0",
organization := "io.whatifs",
scalaVersion := "2.12.8",
Expand All @@ -34,9 +34,9 @@ lazy val headerSettings = Seq(
)

lazy val projectDependencies = Seq(
compilerPlugin("org.typelevel" %% "kind-projector" % "0.10.3"),

"org.scalatest" %% "scalatest" % "3.0.8" % "test",
compilerPlugin("org.typelevel" %% "kind-projector" % "0.10.3"),
"eu.timepit" %% "refined" % "0.9.8",
"org.scalatest" %% "scalatest" % "3.0.8" % "test",
"org.scalacheck" %% "scalacheck" % "1.14.0" % "test"
)

Expand Down
19 changes: 19 additions & 0 deletions HeartRate/notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Notes

## Type Refinement Video Title Ideas

* What if your compiler was your proof assistant
* What if your compiler can predict your runtime errors
* What if your compiler was your static analysis

## Alternative Languages

* Think about duplicating the HeartRates code in C# and javascript.

## Dramatic oneliner snippets

Also think about more dramatic shorter videos with close to 0 domain knowledge requirements.

### Headline Ideas

* Lines of Code that will Change Your Life
5 changes: 5 additions & 0 deletions HeartRate/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
logLevel := Level.Warn

resolvers += Resolver.sonatypeRepo("releases")

addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.0")
37 changes: 37 additions & 0 deletions HeartRate/src/main/scala/ArchivalMeassurement.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.whatifs.heartrates

import eu.timepit.refined._, api.{Refined, Validate}

import java.time.LocalDate

case class ArchivalDatePredicate()
case class ArchivalPredicate[DT](dt: DT)

object ArchivalDatePredicate {
implicit def archivalDatePredicateValidate: Validate.Plain[LocalDate, ArchivalDatePredicate] =
Validate.fromPartial(_.isBefore(LocalDate.now()), "ArchivalDate", ArchivalDatePredicate())
}

object ArchivalPredicate {
implicit def archivalDateValidate[DT <: LocalDate](implicit ws: W.Aux[DT]): Validate.Plain[Measurement, ArchivalPredicate[DT]] =
Validate.fromPredicate(m => !m.date.isAfter(ws.value), t => s"""$t.archivalDate(${ws.value})""", ArchivalPredicate(ws.value))
}

case class ArchivedMeasurement private(measurement: Measurement, archiveDate: ArchivedMeasurement.ArchivalDate) {

def this(archiveDate: ArchivedMeasurement.ArchivalDate)(
measurement: ArchivedMeasurement.ArchivalMeasurement[archiveDate.value.type]) =
this(measurement.value, archiveDate)
}

object ArchivedMeasurement {

type ArchivalDate = LocalDate Refined ArchivalDatePredicate
type ArchivalMeasurement[DT] = Measurement Refined ArchivalPredicate[DT]

def archivedMeasurement(archiveDate: LocalDate, measurement: Measurement): Either[String, ArchivedMeasurement] = for {
dt <- refineV[ArchivalDatePredicate](archiveDate)
m <- refineV[ArchivalPredicate[dt.value.type]](measurement)
} yield new ArchivedMeasurement(dt)(m)

}
21 changes: 21 additions & 0 deletions HeartRate/src/main/scala/HeartRate.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.whatifs.heartrates

import eu.timepit.refined.{W, refineV}
import eu.timepit.refined.api.Refined
import eu.timepit.refined.numeric.Interval

case class HeartRate(bpm: Int Refined HeartRate.ValidHeartRate) {
def isHigh = bpm.value < 100
}
object HeartRate {
final val MinHeartRate = 60
final val MaxHeartRate = 300

type ValidHeartRate = Interval.Closed[W.`MinHeartRate`.T, W.`MaxHeartRate`.T]

def build(bpm: Int): Either[String, HeartRate] = {
refineV[ValidHeartRate](bpm)
.map(HeartRate.apply)
.left.map(_ => "Illegal heart rate")
}
}
25 changes: 25 additions & 0 deletions HeartRate/src/main/scala/Meassurement.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.whatifs.heartrates

import java.time.LocalDate

case class Person(name: String, age: Int)

case class Measurement(person: Person, heartRate: HeartRate, date: LocalDate) {
import Measurement._
def warn: Option[String] =
if (heartRate.isHigh && date.isAfter(warnIfAfter))
Some("Heart Rate is High")
else None

}

object Measurement {
val WarningDays = 3

def measure(person: Person, heartRate: HeartRate): Measurement = {
new Measurement(person, heartRate, LocalDate.now())
}

def warnIfAfter: LocalDate =
LocalDate.now().minusDays(WarningDays.toLong)
}
23 changes: 23 additions & 0 deletions HeartRate/src/test/scala/ArchivalMeassurementSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.whatifs.heartrates

import org.scalatest.{Matchers, PropSpec}
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks

import java.time.LocalDate

class ArchivedMeasurementSpec
extends PropSpec
with ScalaCheckDrivenPropertyChecks
with Matchers {
import HeartRateGenerators._

property ("Measurements can be archived according to the right rules") {
forAll (dateGen, measurementGen) {
case (archiveDate, measurement) if !archiveDate.isBefore(measurement.date) && archiveDate.isBefore(LocalDate.now()) =>
ArchivedMeasurement.archivedMeasurement(archiveDate, measurement).isRight
case (archiveDate, measurement) =>
ArchivedMeasurement.archivedMeasurement(archiveDate, measurement).isLeft
}
}

}
35 changes: 35 additions & 0 deletions HeartRate/src/test/scala/HearRateGenerators.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.whatifs.heartrates

import java.time.LocalDate

import org.scalacheck.Gen

import eu.timepit.refined.refineV

object HeartRateGenerators {
val specialBPMValues: Seq[Int] =
Seq(HeartRate.MinHeartRate, HeartRate.MaxHeartRate)
.flatMap(bpm => Seq(bpm - 1, bpm, bpm + 1))

val bpmGen: Gen[Int] = Gen.chooseNum(Int.MinValue, Int.MaxValue, specialBPMValues:_*).label("bpm")

val hrGen: Gen[HeartRate] = Gen.chooseNum(HeartRate.MinHeartRate, HeartRate.MaxHeartRate)
.map(refineV[HeartRate.ValidHeartRate](_))
.flatMap(_.fold(_ => Gen.fail, bpm => Gen.const(HeartRate.apply(bpm))))

val personGen: Gen[Person] = for {
name <- Gen.alphaStr
age <- Gen.chooseNum(10, 120)
} yield Person(name, age)

val epochDayGen = Gen.chooseNum(LocalDate.MIN.toEpochDay, LocalDate.MAX.toEpochDay)

val dateGen = epochDayGen.map(LocalDate.ofEpochDay)

val measurementGen: Gen[Measurement] = for {
person <- personGen
hr <- hrGen
dt <- dateGen
} yield Measurement(person, hr, dt)

}
38 changes: 38 additions & 0 deletions HeartRate/src/test/scala/HeartRateSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.whatifs.heartrates

import org.scalatest.{Matchers, PropSpec}
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
import eu.timepit.refined.refineV
import eu.timepit.refined.auto._

class HeartRateSpec
extends PropSpec
with ScalaCheckDrivenPropertyChecks
with Matchers {

import HeartRateGenerators._

property ("When building HeartRate, bounds are respected") {
forAll(bpmGen) {
case bpm if (bpm < HeartRate.MinHeartRate) =>
assert(HeartRate.build(bpm).isLeft)

case bpm if (bpm > HeartRate.MaxHeartRate) =>
assert(HeartRate.build(bpm).isLeft)

case bpm =>
val hr = refineV[HeartRate.ValidHeartRate](bpm).map(HeartRate.apply(_))
assert(hr.isRight)
assertResult(hr) {
HeartRate.build(bpm)
}
}
}

property ("HeartRate compiles only for legal values") {
HeartRate(120)
assertCompiles("HeartRate(120)")
assertTypeError("HeartRate(-100)")
}

}
32 changes: 32 additions & 0 deletions HeartRate/src/test/scala/MeassurementSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.whatifs.heartrates

import java.time.LocalDate

import org.scalatest.{Matchers, PropSpec}
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks

class MeassurementSpec
extends PropSpec
with ScalaCheckDrivenPropertyChecks
with Matchers {

import HeartRateGenerators._

property ("Measurements are done same day") {
forAll(personGen, hrGen) { (person, hr) =>
val measurement = Measurement.measure(person, hr)
assertResult(Measurement(person, hr, LocalDate.now())) {
measurement
}
assert(measurement.warn.isDefined == hr.isHigh, "today measurement warning <==> high heart rate")
}
}

property ("Measurement warning properties") {
forAll (measurementGen) { measurement =>
assertResult(measurement.heartRate.isHigh && measurement.date.isAfter(Measurement.warnIfAfter))
{ measurement.warn.isDefined }
}
}

}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ They are not designed to teach you a technique but to show you an extreme from w
2. What if you never write a function longer than 3 lines?

3. What if you write non-implementation-specific scenarios in BDD?

4. What if you write all your unit-tests as property based tests?
1 change: 0 additions & 1 deletion propertybasedtesting/README.md

This file was deleted.

7 changes: 0 additions & 7 deletions propertybasedtesting/project/plugins.sbt

This file was deleted.

9 changes: 0 additions & 9 deletions propertybasedtesting/src/main/scala/App.scala

This file was deleted.

17 changes: 0 additions & 17 deletions propertybasedtesting/src/test/scala/AppSpec.scala

This file was deleted.