Skip to content
Prev Previous commit
Next Next commit
Updating the documentation to use tut
  • Loading branch information
alexflav23 committed Nov 25, 2018
commit e54ed87d49ec664adf86280cf8389401fc898afb
4 changes: 4 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -431,4 +431,8 @@ lazy val readme = (project in file("readme"))
testingTwitter,
macros,
tags
).settings(
libraryDependencies ++= Seq(
"org.scalatest" %% "scalatest" % Versions.scalatest % "tut"
)
).enablePlugins(TutPlugin)
186 changes: 106 additions & 80 deletions readme/src/main/tut/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,63 +86,6 @@ heavily throughout the Outworkers ecosystem of projects, from internal to DSL mo
performance gain in code.


### Async assertions ###
<a href="#table-of-contents">Back to top</a>


The async assertions module features a dual API, so you can call the same methods on both ```scala.concurrent.Future``` and ```com.twitter.util.Future```.
The underlying mechanism will create an async ```Waiter```, that will wait for the future to complete within the given ```PatienceConfiguration```. The
awaiting is done asynchronously and the assertions are invoked and evaluated once the future in question has returned a result.

```tut:silent
import com.outworkers.util.testing._

class MyTests extends FlatSuite with Matchers {

"The async computation" should "return 0 on completion" in {
val f: Future[Int] = .. // Pretend this is a Future just like any other future.
f.successful {
res => {
res shouldEqual 0
}
}
}

"This async computation" should "fail by design" in {
val f: Future[Unit] = ..

// You don't even need to do anything more than failure at this stage.
// If the Future fails, the test will succeed, as this method is used when you "expect a failure".
// You can however perform assertions on the error returned.
f.failing {
err => {
}
}
}

"This async computation" should "fail with a specific error" in {
val f: Future[Unit] = ..
f.failingWith[NumberFormatException] {
err => {
}
}
}

}
```


You can directly customise the ```timeout``` of all ```Waiters``` using the ScalaTest specific time span implementations and interval configurations.


```scala
import org.scalatest.concurrent.PatienceConfiguration
import org.scalatest.time.SpanSugar._

implicit val timeout: PatienceConfiguration.Timeout = timeout(20 seconds)

```

Summary:

- The dependency you need is ```"com.outworkers" %% "util-testing" % UtilVersion```.
Expand All @@ -161,11 +104,15 @@ After you define such a one-time sampling type class instance, you have access t
It's useful to define such typeclass instances inside package objects, as they will be "invisibly" imported in to the scope you need them to. This is often really neat, albeit potentially confusing for novice Scala users.


```tut:passthrough
```tut:silent

import com.outworkers.util.testing._

@sample case class MyAwesomeClass(name: String, age: Int, email: String)
case class MyAwesomeClass(
name: String,
age: Int,
email: String
)
```

You may notice this pattern is already available in better libraries such as ScalaMock and we are not trying to provide an alternative to ScalaMock or compete with it in any way. Our typeclass generator approach only becomes very useful where you really care about very specific properties of the data.
Expand All @@ -179,13 +126,15 @@ It's also useful when you want to define specific ways in which hierarchies of c
<a href="#table-of-contents">Back to top</a>

One interesting thing that happens when using the `@sample` annotation is that using `gen` immediately after it will basically
give you an instance of your `case class` with the fields appropiately pre-filled, and some of the basic scenarios are also name aware.
give you an instance of your `case class` with the fields appropriately pre-filled, and some of the basic scenarios are also name aware.

What this means is that we try to make the data feel "real" with respect to what it should be. Let's take the below example:

```tut:passthrough
```tut:silent

import java.util.UUID

@sample case class User(
case class User(
id: UUID,
firstName: String,
lastName: String,
Expand All @@ -194,20 +143,23 @@ What this means is that we try to make the data feel "real" with respect to what
```
This is interesting and common enough. What's more interesting is the output of `gen`.

```tut:passthrough

val user = gen[User]

user.trace()
```tut:silent
import com.outworkers.util.samplers._

/**
User(
id = 6be8914c-4274-40ee-83f5-334131246fd8
firstName = Lindsey
lastName = Craft
email = [email protected]
)
*/
object Examplers {
val user = gen[User]

user.trace()

/**
User(
id = 6be8914c-4274-40ee-83f5-334131246fd8
firstName = Lindsey
lastName = Craft
email = [email protected]
)
*/
}

```

Expand All @@ -219,6 +171,78 @@ During the macro expansion phase, we check the annotation targets and try to inf
if your field name is either "email" or "emailAddress" or anything similar enough, you will get an "email" back.


It is also possible to generate deeply nested case classes.

```tut:silent

case class Address(
postcode: String,
firstLine: String,
secondLine: String,
thirdLine: String,
city: String,
country: String
)

case class GeoLocation(
longitude: BigDecimal,
latitude: BigDecimal
)

case class LocatedUser(
geo: GeoLocation,
address: Address,
user: User
)

object GenerationExamples {
val deeplyNested = gen[LocatedUser]
}

```

#### Extending the sampling capability.

The automated sampling capability is a fairly simple but useful party trick. It relies
on the framework knowing how to generate basic things, such as `Int`, `Boolean`, `String`,
and so on, and the framework can then compose from these samplers to build them up
into any hierarchy of case classes you need.

But sometimes it will come short when it doesn't know how to generate a specific type. For example,
let's look at how we could deal with `java.sql.Date`, which has no implicit sample available by default.

```tut:silent

case class ExpansionExample(
id: UUID,
date: java.sql.Date
)
```

Let's try to write some tests around the sampler. All we need to do is create a sampler for `java.sql.Date`.

```tut:silent

import org.scalatest.{ FlatSpec, Matchers }
import org.joda.time.DateTime

class MyAwesomeSpec extends FlatSpec with Matchers {

implicit val sqlDateSampler: Sample[java.sql.Date] = Sample.iso[DateTime]

"The samplers lib" should "automatically sample an instance of ExpansionExample" in {
val instance = gen[ExpansionExample]
}
}

```



#### Working with options.



### Generating data

There are multiple methods available, allowing you to generate more than just the type:
Expand Down Expand Up @@ -330,14 +354,16 @@ import scalaz._
import scalaz.Scalaz._
import com.outworkers.util.parsers._

case class UserToRegister(
email: String,
age: Int
)

object Test {

def registerUser(str: String, age: String): Unit = {
def registerUser(str: String, age: String): Validation[NonEmptyList[String], UserToRegister] = {
(parse[EmailAddress](str) |@| parse[Int](age)) {
(validEmail, validAge) => {
}
}.fold {
// ..
(validEmail, validAge) => UserToRegister(validEmail.value, validAge)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class PlayAugmenterTests extends FlatSpec

implicit val defaultTimeout: PatienceConfiguration.Timeout = timeout(defaultTimeoutSpan)

override implicit val patienceConfig = PatienceConfig(
override implicit val patienceConfig: PatienceConfig = PatienceConfig(
timeout = defaultTimeoutSpan,
interval = Span(defaultScalaInterval, Millis)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ trait Generators {
builder.result()
}



def genList[T : Sample](size: Int = defaultGeneration): List[T] = gen[List, T](size)

def genSet[T : Sample](size: Int = defaultGeneration): Set[T] = gen[Set, T](size)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
package com.outworkers.util.samplers

trait FillOptions
trait FillOptions {
def apply[T : Sample](opt: Option[T]): Option[T]
}

object Options {
implicit val alwaysFillOptions: FillOptions = new FillOptions {}
implicit val alwaysFillOptions: FillOptions = new FillOptions {
override def apply[T: Sample](opt: Option[T]): Option[T] = {
opt.orElse(Some(gen[T]))
}
}
implicit val neverFillOptions: FillOptions = new FillOptions {
override def apply[T: Sample](opt: Option[T]): Option[T] = {
Option.empty[T]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ class SamplerMacro(val c: blackbox.Context) extends AnnotationToolkit with Black
applier = applied => TypeName(s"scala.Option[..$applied]"),
generator = t => {
if (fillOptionsImp.nonEmpty) {
q"""$prefix.getConstOpt[..$t]"""
q"""$fillOptionsImp($prefix.genOpt[..$t])"""
} else {
q"""$prefix.genOpt[..$t]"""
}
Expand Down Expand Up @@ -235,7 +235,7 @@ class SamplerMacro(val c: blackbox.Context) extends AnnotationToolkit with Black
val fillOptionsImp = c.inferImplicitValue(fillOptions, silent = true)

if (fillOptionsImp.nonEmpty) {
q"""$prefix.getConstOpt[$derived].map(_.value)"""
q"""$fillOptionsImp($prefix.genOpt[$derived]).map(_.value)"""
} else {
q"""$prefix.genOpt[$derived].map(_.value)"""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import com.outworkers.util.samplers.Generators
import com.outworkers.util.tags.DefaultTaggedTypes
import org.joda.time.{DateTime, DateTimeZone, LocalDate}
import org.scalacheck.Gen
import org.scalatest.Assertions
import org.scalatest.{Assertion, Assertions}
import org.scalatest.concurrent.{PatienceConfiguration, ScalaFutures, Waiters}
import org.scalatest.exceptions.TestFailedException

Expand Down Expand Up @@ -74,63 +74,4 @@ package object testing extends ScalaFutures
ScalaAwait.result(future, duration)
}
}

/**
* Augmentation to allow asynchronous assertions of a @code {scala.concurrent.Future}.
* @param f The future to augment.
* @tparam A The underlying type of the computation.
*/
implicit class ScalaFutureAssertions[A](val f: ScalaFuture[A]) extends Assertions with Waiters {

/**
* Use this to assert an expected asynchronous failure of a @code {com.twitter.util.Future}
* The computation and waiting are both performed asynchronously.
* @param mf The class Manifest to extract class information from.
* @param timeout The timeout of the asynchronous Waiter.
* @tparam T The error returned by the failing computation. Used to assert error messages.
*/
def failing[T <: Throwable](
implicit mf: Manifest[T],
timeout: PatienceConfiguration.Timeout,
ec: ExecutionContext
): Unit = {
val w = new Waiter

f onComplete {
case Success(_) => w.dismiss()
case Failure(e) => w(throw e); w.dismiss()
}

intercept[T] {
w.await(timeout, dismissals(1))
}
}

def failingWith[T <: Throwable](fs: ScalaFuture[_]*)(implicit mf: Manifest[T], ec: ExecutionContext) {
val w = new Waiter
fs foreach (_ onComplete {
case Failure(er) =>
w(intercept[T](er))
w.dismiss()
case Success(_) => w.dismiss()
})
w.await()
}

/**
* Use this to assert a successful future computation of a @code {com.twitter.util.Future}
* @param x The computation inside the future to await. This waiting is asynchronous.
* @param timeout The timeout of the future.
*/
@deprecated("Use ScalaTest AsyncAssertions trait instead", "0.31.0")
def successful(x: A => Unit)(implicit timeout: PatienceConfiguration.Timeout, ec: ExecutionContext) : Unit = {
val w = new Waiter

f onComplete {
case Success(res) => w{x(res)}; w.dismiss()
case Failure(e) => w(throw e); w.dismiss()
}
w.await(timeout, dismissals(1))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.outworkers.util.testing

import org.scalatest.{FlatSpec, Matchers}

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

class AssertionsTest extends FlatSpec with Matchers {

it should "correctly assert a failure in a failing test" in {
val msg = gen[ShortString].value
val f = Future { throw new Exception(msg)}

f.failing { err =>

}
}

}