diff --git a/E4-PropertyBasedTests b/E4-PropertyBasedTests new file mode 120000 index 0000000..8c53209 --- /dev/null +++ b/E4-PropertyBasedTests @@ -0,0 +1 @@ +HeartRate \ No newline at end of file diff --git a/propertybasedtesting/.gitignore b/HeartRate/.gitignore similarity index 100% rename from propertybasedtesting/.gitignore rename to HeartRate/.gitignore diff --git a/HeartRate/.projectile b/HeartRate/.projectile new file mode 100644 index 0000000..e69de29 diff --git a/propertybasedtesting/.scalafmt.conf b/HeartRate/.scalafmt.conf similarity index 84% rename from propertybasedtesting/.scalafmt.conf rename to HeartRate/.scalafmt.conf index bf2da0b..2321096 100644 --- a/propertybasedtesting/.scalafmt.conf +++ b/HeartRate/.scalafmt.conf @@ -1,3 +1,4 @@ +version = "2.0.0" style = defaultWithAlign maxColumn = 120 project.git = true diff --git a/propertybasedtesting/LICENSE b/HeartRate/LICENSE similarity index 100% rename from propertybasedtesting/LICENSE rename to HeartRate/LICENSE diff --git a/HeartRate/README.md b/HeartRate/README.md new file mode 100644 index 0000000..422bdb1 --- /dev/null +++ b/HeartRate/README.md @@ -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. \ No newline at end of file diff --git a/propertybasedtesting/build.sbt b/HeartRate/build.sbt similarity index 96% rename from propertybasedtesting/build.sbt rename to HeartRate/build.sbt index 8a619c1..6f82056 100644 --- a/propertybasedtesting/build.sbt +++ b/HeartRate/build.sbt @@ -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", @@ -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" ) diff --git a/HeartRate/notes.md b/HeartRate/notes.md new file mode 100644 index 0000000..26de8dd --- /dev/null +++ b/HeartRate/notes.md @@ -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 diff --git a/propertybasedtesting/project/build.properties b/HeartRate/project/build.properties similarity index 100% rename from propertybasedtesting/project/build.properties rename to HeartRate/project/build.properties diff --git a/HeartRate/project/plugins.sbt b/HeartRate/project/plugins.sbt new file mode 100644 index 0000000..2bf429c --- /dev/null +++ b/HeartRate/project/plugins.sbt @@ -0,0 +1,5 @@ +logLevel := Level.Warn + +resolvers += Resolver.sonatypeRepo("releases") + +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.0") diff --git a/HeartRate/src/main/scala/ArchivalMeassurement.scala b/HeartRate/src/main/scala/ArchivalMeassurement.scala new file mode 100644 index 0000000..3da2441 --- /dev/null +++ b/HeartRate/src/main/scala/ArchivalMeassurement.scala @@ -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) + +} diff --git a/HeartRate/src/main/scala/HeartRate.scala b/HeartRate/src/main/scala/HeartRate.scala new file mode 100644 index 0000000..432a477 --- /dev/null +++ b/HeartRate/src/main/scala/HeartRate.scala @@ -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") + } +} diff --git a/HeartRate/src/main/scala/Meassurement.scala b/HeartRate/src/main/scala/Meassurement.scala new file mode 100644 index 0000000..0a961a5 --- /dev/null +++ b/HeartRate/src/main/scala/Meassurement.scala @@ -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) +} diff --git a/HeartRate/src/test/scala/ArchivalMeassurementSpec.scala b/HeartRate/src/test/scala/ArchivalMeassurementSpec.scala new file mode 100644 index 0000000..d6388f1 --- /dev/null +++ b/HeartRate/src/test/scala/ArchivalMeassurementSpec.scala @@ -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 + } +} + + } \ No newline at end of file diff --git a/HeartRate/src/test/scala/HearRateGenerators.scala b/HeartRate/src/test/scala/HearRateGenerators.scala new file mode 100644 index 0000000..bc8c40f --- /dev/null +++ b/HeartRate/src/test/scala/HearRateGenerators.scala @@ -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) + +} diff --git a/HeartRate/src/test/scala/HeartRateSpec.scala b/HeartRate/src/test/scala/HeartRateSpec.scala new file mode 100644 index 0000000..c1a4b9b --- /dev/null +++ b/HeartRate/src/test/scala/HeartRateSpec.scala @@ -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)") + } + +} diff --git a/HeartRate/src/test/scala/MeassurementSpec.scala b/HeartRate/src/test/scala/MeassurementSpec.scala new file mode 100644 index 0000000..cc1c539 --- /dev/null +++ b/HeartRate/src/test/scala/MeassurementSpec.scala @@ -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 } + } + } + +} \ No newline at end of file diff --git a/README.md b/README.md index fff8cc3..c05d164 100644 --- a/README.md +++ b/README.md @@ -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? diff --git a/propertybasedtesting/README.md b/propertybasedtesting/README.md deleted file mode 100644 index 1398299..0000000 --- a/propertybasedtesting/README.md +++ /dev/null @@ -1 +0,0 @@ -Simple Sbt project with Scala. \ No newline at end of file diff --git a/propertybasedtesting/project/plugins.sbt b/propertybasedtesting/project/plugins.sbt deleted file mode 100644 index 9be3b57..0000000 --- a/propertybasedtesting/project/plugins.sbt +++ /dev/null @@ -1,7 +0,0 @@ -logLevel := Level.Warn - -resolvers += Resolver.sonatypeRepo("releases") - -//addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.1") - -//addSbtPlugin("de.heikoseeberger" % "sbt-header" % "2.0.0") diff --git a/propertybasedtesting/src/main/scala/App.scala b/propertybasedtesting/src/main/scala/App.scala deleted file mode 100644 index e1c210c..0000000 --- a/propertybasedtesting/src/main/scala/App.scala +++ /dev/null @@ -1,9 +0,0 @@ -package io.whatifs.propertybasedtesting - -case class HeartRate(bpm: Int); - -object HeartRate { - def build(bpm: Int): Either[String, HeartRate] = { - Right(HeartRate(bpm)) - } -} \ No newline at end of file diff --git a/propertybasedtesting/src/test/scala/AppSpec.scala b/propertybasedtesting/src/test/scala/AppSpec.scala deleted file mode 100644 index 8c13e71..0000000 --- a/propertybasedtesting/src/test/scala/AppSpec.scala +++ /dev/null @@ -1,17 +0,0 @@ -package io.whatifs.propertybasedtesting - -import org.scalatest.{Matchers, PropSpec} -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - -class CheckSpec - extends PropSpec - with ScalaCheckDrivenPropertyChecks - with Matchers { - - property ("Heart Rates are always within range") { - forAll { (bpm: Int) => - bpm == 1 - } - } - -}