Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use consistent solution to customize tests, suites and values #64

Merged
merged 2 commits into from
Mar 14, 2020

Conversation

olafurpg
Copy link
Member

@olafurpg olafurpg commented Mar 10, 2020

Opening this as an alternative proposal to #61 (cc/ @kubukoz).

Previously, users could override several methods to customize how tests
were transformed, how suites filtered tests and how values got lifted
into futures. While this system worked OK, it wasn't easy to explain
since it was quite inconsistent.

Now, this commit introduces three consistent solutions to transform:

  • test cases: a function Test => Test
  • test suites: a function List[Test] => List[Test]
  • test values: a partial function Any => Future[Any]

Users register new transforms using a similar API as you register fixtures:

  override def munitTestTransforms: List[TestTransform] =
    super.munitTestTransforms ++ List(
      new TestTransform("append scala version", { test =>
        test.withName(test.name + "-" + scalaVersion)
      })
    )

Opening this as a draft PR to see what people think. It's quite a breaking change but at the moment I feel like it's a nice improvement.

@olafurpg
Copy link
Member Author

The idea to register transforms using the same syntax as registering tests comes from JavaScript libraries like Jest/Ava, see https://jestjs.io/docs/en/setup-teardown

I started with the following syntax

override def munitTestTransforms = super.munitTestTransforms :+ {
test => ... }

but it's quite overwhelming to type, it's easy to get wrong and it's not IDE friendly.

@gabro
Copy link
Member

gabro commented Mar 10, 2020

How do multiple transforms combine?

transformValue("Lift task to future") {
  case LazyFuture(runFuture()) => runFuture()
}

transformValue("Another one") {
  case LazyFuture(runFuture()) => Future.successful(42)
}

I'm all for expressive APIs, but I'm a bit afraid that the mutable nature of the underlying representation will leak if we allow order-dependent transformations.

Overriding a method has the benefit of ensuring uniqueness of the transformation and I think it's a more natural API in the JVM world, where we have classes (as opposed to JS frameworks where classes are very different and seldom used in public APIs)

@olafurpg
Copy link
Member Author

Transforms are indeed order-dependent, and I agree it may get tricky to reason about the order. Having tried both approaches however, I feel that it's really easy to mess up the override logic and for example accidentally remove the default transforms (lift future). It's quite tricky to debug what's wrong in these cases.

One possible way to avoid the ordering issues would be to allow registering a "priority" alongside transforms. MUnit would sort the transforms according to the priority before applying them.

transformValue("Lift task to future", Priority.High) {
  case LazyFuture(runFuture()) => runFuture()
}

transformValue("Another one", Priority.Low) {
  case LazyFuture(runFuture()) => Future.successful(42)
}

Would that ease your concerns?

@olafurpg
Copy link
Member Author

We can BTW record munit.Location for transforms and print a detailed trace of what transforms get applied in what order under some debug flag. This could help shed light on what's going on if something is not working as expected.

@olafurpg
Copy link
Member Author

After thinking more about it, I am also open to remove the mutable transformValue("name") { .. } helpers but keep the other changes. The override def syntax is not that bad after introducing the SuiteTransform TestTransform ValueTransform classes.

override val valueTransforms = List(
  suiteTransform("a") { ... },
  ...
)

It would be nice to have a splice operator like in JS, however 🤔

@gabro
Copy link
Member

gabro commented Mar 10, 2020

I feel like adding priority is just going to make the whole thing even more difficult to reason about (now I need to look at priorities to reconstruct the order in my head).

I like the other changes, but I much prefer to have a single entry point for this API (the override syntax of your last post looks good enough to me)

@olafurpg
Copy link
Member Author

Sounds good, I agree the priorities only complicate things. I'll remove the mutable syntax to register transforms.

@olafurpg olafurpg marked this pull request as ready for review March 11, 2020 08:43
@olafurpg
Copy link
Member Author

I removed the mutable transformTest(...) syntax so that it's now override based instead

  override def munitTestTransforms: List[TestTransform] =
    super.munitTestTransforms ++ List(
      new TestTransform("append scala version", { test =>
        test.withName(test.name + "-" + scalaVersion)
      })
    )

I didn't add helper methods to construct transforms because I'm not sure if it's helpful. A plain Scala class felt simple enough to explain and in Scala 3 the new keyword won't be needed.

@olafurpg
Copy link
Member Author

@kubukoz the code to handle a lazy future is now like this

  case class LazyFuture[+T](run: () => Future[T])
  object LazyFuture {
    def apply[T](thunk: => T)(implicit ec: ExecutionContext): LazyFuture[T] =
      LazyFuture(() => Future(thunk))
  }

  override def munitValueTransforms: List[ValueTransform] =
    super.munitValueTransforms ++ List(
      new ValueTransform("LazyFuture", {
        case LazyFuture(run) => run()
      })
    )

This API makes it easier to the right thing and harder to the wrong thing. No more fiddling with => Any or mixing up the order to call super.munitRunTest. What do you think?

Previously, users could override several methods to customize how tests
were transformed, how suites filtered tests and how values got lifted
into futures. While this system worked OK, it wasn't easy to explain
since it was quite inconsistent.

Now, this commit introduces three consistent solutions to transform:

* test cases: a function `Test => Test`
* test suites: a function `List[Test] => List[Test]`
* test values: a partial function `Any => Future[Any]`

Users register new transforms using a similar API as you register
fixtures:

```scala
override def munitTestTransforms: List[TestTransform] =
  super.munitTestTransforms ++ List(
    new TestTransform("append scala version", { test =>
      test.withName(test.name + "-" + scalaVersion)
    })
  )
```
@olafurpg
Copy link
Member Author

I'll give this a few days to brew before merging since it's a big breaking change.

@olafurpg
Copy link
Member Author

A side-effect of this change is that value transforms automatically flatten custom Task-like types. Previously, the case below was difficult to handle with a custom munitTestValue but with value transforms the lazy future is automatically flattened (for better or worse)

  test("nested".fail) {
    LazyFuture {
      LazyFuture {
        // Test will fail because  LazyFuture.run()` is automatically called
        throw new RuntimeException("BOOM!")
      }
    }
  }

@olafurpg
Copy link
Member Author

@kubukoz I'm happy to help migrate code to the new APIs if you have already started working on a cats-effect integration or something like that. I really want to stabilize the MUnit API as soon as possible to avoid further breaking changes.

@olafurpg
Copy link
Member Author

FWIW, following up from an offline conversation with @gabro I'm open to host integration modules (cats-effect, hedgehog, scalacheck, zio, ...) in the MUnit repo. I believe that would make it easier to deal with breaking changes like this

@MrPowers
Copy link

The override based syntax looks great.

@olafurpg @gabro - great job collaborating & making a great interface!

@kubukoz
Copy link

kubukoz commented Mar 11, 2020

I'll try to read this again in a couple hours, sorry for being late 😅

@olafurpg
Copy link
Member Author

@kubukoz no rush, take your time. I updated the existing docs but didn't add any new docs. Please let me know if there's anything unclear

@kubukoz
Copy link

kubukoz commented Mar 13, 2020

I'm fairly satisfied with overriding, can't foresee any problemws with that at this point, so you have my blessing :)

as for a CE integration, there really isn't that much code and it'll be a low cost migration. It's pretty much what the LazyFuture override does.

And I'm definitely up for having a module in the repo :)

@olafurpg
Copy link
Member Author

Thanks! I'll cut a milestone release to see if there's an unexpected regression

@olafurpg olafurpg merged commit 577fd68 into scalameta:master Mar 14, 2020
@olafurpg olafurpg deleted the new-test branch March 14, 2020 07:24
@olafurpg
Copy link
Member Author

0.6.0-M1 is out with these changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants