Skip to content

Instantly share code, notes, and snippets.

@etorreborre
Created August 12, 2021 16:43
Show Gist options
  • Save etorreborre/eb84cb5ea40fc8f296e401713cfee50f to your computer and use it in GitHub Desktop.
Save etorreborre/eb84cb5ea40fc8f296e401713cfee50f to your computer and use it in GitHub Desktop.
Response to "My specs(3?) wishlist"

Prescriptive Structural Syntax

I am sorry but I actually prefer the other style of specification! The main reason is that I think that encourages me to think about writing some text about what I am testing, and why I am testing it this way (that doesn't mean that I necessarily do it :-)).

Enforced Matcher Uniformity

I hear you and I indeed simplified all must expressions to be a must b, and then a must not(b).

Better Matcher Construction

I have added a bunch of Matcher.apply functions but also simplified the Matcher data type which does not longer return a MatchResult[T] but simply a Result. The reason for having the intermediate MatchResult[T] data type was to keep the actual value around and to have both a success and a failure message which could be switched when calling not(matcher).

It turns out that it is really hard to compose failure messages when trying to create matchers from other matchers so I gave up on that. I eventually think that it is easier to reuse the success/failure logic of other matchers but draft your own message for a custom matcher.

First-Class Support for Async Tests

specs2 now supports Future[T] as the body of a test, as long as T has a AsResult[T] instance. But more generally there is an AsExecution typeclass which can be used to transform the body of any async example using cats IO or ZIO to a specs2 Execution. The cats IO integration itself has now be put in a separate specs2-cats project.

Async ScalaCheck

The support for ScalaCheck in specs2 is a bit convoluted and while I think it should be possible to do something for async properties I haven't tried yet.

Better Support for things like Discipline

I would need a concrete test project showing one of the issues. It seems to me that Discipline is doing to the right thing by creating one example per property. Then everything else should behave like a normal specification with sequential execution if sequential is set etc...

The seed issue could be a bug due to the way I am capturing the actual seed being used to report it. That's something I can definitely investigate if you have a way to reliably reproduce the problem.

Better Support for Test Abstraction

You can add / remove fragments even with a mutable specification. To do this you can override the inherited map or flatMap functions:

  /** modify the whole list of fragments */
  def map(fs: =>Fragments): Fragments = fs

  /** modify each fragment to expand it into a list of fragments */
  def flatMap(f: Fragment): Fragments = Fragments(f)

Would that help for what you'd like to do?

Slightly Saner pendingUntilFixed/etc Combinators

This one might be a naming thing. pending and skipped are Results while pendingUntilFixed is a way to modify the execution of a Result. If that helps I have added the possibility to use the pendingUntilFixed method as a prefix method (also available in the next specs2-4.x version):

"example 1" >> pendingUntilFixed {
  1 === 2
}

"example 1" >> pendingUntilFixed("ISSUE-123") {
  1 === 2
}

Maybe this is "slightly saner" :-)?

Integrated Lifecycle Management

Indeed before/after methods are very "side-effecty". In this new version of specs2 there is a Resource[T] trait which can be used to declare a resource available to all the examples a specification.

For example you can mix the following trait to your specification:

trait DbConnection(using ec: ExecutionContext) extends Resource[Connection]:
  def acquire: Future[Connection] =
    ??? // create a new connection

  def release(connection: Connection): Execution = Execution.future {
    connection.close.map {
      case Right(_) => success
      case Left(e) => anError(e)
    }
  }  

class MySpec(using ec: ExecutionContext) extends mutable.Specification, DbConnection:
  "use a database connection" >> { (c: Connection) =>
     c.executeSql("SELECT COUNT(*) FROM TABLE").map { n => n must be_>=(10) }
  }

You can even share that resource across several specifications by giving it a resource key:

trait DbConnection(using ec: ExecutionContext) extends Resource[Connection]:
  override def resourceKey: Option[String] =
    Some("DB connection")

  def acquire: Future[Connection] =
    ...

  def release(connection: Connection): Execution =
    ...

Then the release method will only be called at the end of a run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment