Skip to content

Instantly share code, notes, and snippets.

@mpilquist
Last active December 31, 2015 16:29
Show Gist options
  • Select an option

  • Save mpilquist/8014394 to your computer and use it in GitHub Desktop.

Select an option

Save mpilquist/8014394 to your computer and use it in GitHub Desktop.
scala> import org.scalatest._
scala> import Matchers._
scala> import Inspectors._
scala> trap { List('a', 'b') should have length 1 }
res0: Throwable = org.scalatest.exceptions.TestFailedException: List(a, b) had length 2 instead of expected length 1
scala> trap { List(1, 2, 3) should contain (4) }
res1: Throwable = org.scalatest.exceptions.TestFailedException: List(1, 2, 3) did not contain element 4
scala> trap { forEvery (List(1, 2, 3)) { x => x should be < 2 } }
res2: Throwable =
org.scalatest.exceptions.TestFailedException: forEvery failed, because:
at index 1, 2 was not less than 2 (<console>:21),
at index 2, 3 was not less than 2 (<console>:21)
in List(1, 2, 3)
scala> import org.scalautils.StringNormalizations._
scala> trap { (Array("Doe", "Me") should contain oneOf ("X", "RAY", "BEAM")) (after being lowerCased) }
res3: Throwable = org.scalatest.exceptions.TestFailedException: Array(Doe, Me) did not contain one of ("X", "RAY", "BEAM")
@bvenners
Copy link

Hi Runar,

I'm curious, is it the operator notation that's throwing you off? How is it that you think figuring out the types for (after being lowerCased) would be harder than for (after.being(lowerCased))? One is a method call on a variable in operator notation, the other in dots and parens notation. In either case you'll need to look up the type of after, which turns out is TheAfterWord, then look at the signature of its being method, which is:

def being[N](normalization: Normalization[N])(implicit equivalence: Equivalence[N]): NormalizingEquivalence[N]

There is indeed one type class required by being, Equivalence, but I'm not sure why you and Paul would call that "implicit magic." Equivalence is very much like Scalaz's Equal typeclass, and oddly enough actually I decided to add this in addition to Equality after my conversation with you about the problems with And and universal equality at the Mexican restaurant! Here's Equivalence:

trait Equivalence[T] {
def areEquivalent(a: T, b: T): Boolean
}

So I'm not sure I see the magic.

Anyway, I actually had the same reaction as Paul when I first saw Hamcrest matchers back in my JUnit days. I was perfectly happy with assertions and didn't see the need for the extra overhead of the Hamcrest matcher DSL. I did what he says he does: I just used bare Boolean expressions in assertions, and only if a test failed would I go back and add a clue string. That worked just fine for me.

But later, I did quite like the matchers in Ruby's RSpec when I saw them, and felt they allowed the writer of the code to communicate intent at a higher level, the level of what I'd say to you in conversation if you asked me about the code: "At this point in the test, the result should be less than 7". That's what I tried as an experiment with Matchers in ScalaTest. I saw the tradeoff being a matcher expression makes it more obvious what the intent of the writer was (so long as the reader understands English), but less obvious how it works. What I've observed in many years of users using these things is that by and large people don't care how a matcher expression works. They figure out how to write them by looking at examples, and when they read them, they just think at the high level of the statement, not at the level of the intermediate types produced by the DSL.

...Except when they get a compiler error message like the one you pasted. That is where all the "magic" bleeds out, but it isn't so much implicit magic as it is just DSL implementation magic. That is indeed a pain. It would be nice if we could teach the compiler to give better error messages in such cases.

Bill

@bvenners
Copy link

Hi Runar,

Perhaps proving your point, I awoke this morning realizing I had grabbed the wrong overloaded being method in my previous comment. Normally the way to figure out which method is called is to follow the types. The type of lowerCased is Uniformity, which is a subtype of Normalization that can also do "universal normalization", i.e., normalize an Any. This was needed because I felt the need to support universal equality via Equality for compatibility reasons. I want ScalaTest to be easily used to test anything that can be thrown at it on the Java platform, including any kind of object written in Java or any other JVM language.

When it came time to add tunable type checks, I at first resisted having two different traits, Equality and Equivalence, and just had Equality. But that meant users would always need to handle Any both for alternate equality implementations and normalizations. That's often a pain, and after our conversation at the Mexican restaurant, and more anguished thinking about it, I decided to just have two traits each. Equality/Uniformity handle Any on the right hand side. Equivalence/Normalization can be used when both left and right are the same type, which === enables. And symmetrically, Equality extends Equivalence; Uniformity extends Normalization. The universal trait is in both cases a subtype of the more restrictive trait.

So the actual being method being called in the (after being lowerCased) case is the other overloaded being method in trait Explicitly, which looks like:

def being[N](uniformity: Uniformity[N])(implicit equality: Equality[N]): NormalizingEquality[N]

Essentially what the two overloaded being methods do is transform a normalization typeclass (either Uniformity or Normalization) into an equality typeclass (either NormalizingEquality or a NormalizingEquivalence) that first normalizes both the left and right sides, and then delegates to the implicitly grabbed equality typeclass (either Equality or Equivalence) to compare the normalized forms.

Bill

@SethTisue
Copy link

we've been taking the approach of using macros ... The plan is, in the next release actually, to give good error messages for Boolean expressions that equate to the built-in matchers

That sounds terrific! Looking forward to it.

(I currently never go near the matchers stuff, because what Paul and Runar said.)

@bvenners
Copy link

Hi Paul,

By the way, in 2.1.0-RC3 I added toEquality and toEquivalance methods to Uniformity and Normalization, so that you need not use the English-like syntax to go, for example, from a Uniformity to an Equality. Instead of:

(result === "hello")(after being lowerCased)

You can just write:

(result === "hello")(lowerCased.toEquality)

I got the memo from multiple sources that even some of the folks who like seeing English-like DSLs in test code don't want to see it in the production code.

Bill

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