Tuesday, November 18, 2008

Equalizer

I wrote a cool Equalizer class in Scala that allows me to do assertions that are more readable than traditional assertions.

Quick side note: (Equalizer is really an idea shamelessly stolen from Bill Venners Equalizer in ScalaTest. Hopefully we'll add my methods in as well.)

My Equalizer lets me do things like:

val x = 5
x mustBe 5

This replaces the more common assertEquals( x, 5 ), and I think it does so nicely.

You can also do a few more sophisticated things with it, like so:

val x = 5
x mustBe ( 3 or 4 or 5 )

I probably could have used "in" here, such as x mustBe in( 3, 4, 5 ) ... what do you think?

Additionally:

val x = 5
x cantBe 6

For whatever this one is worth (I've actually found use cases for it):

val x = false
x canBeNullOr false

val y = 6
y canBeNullOr ( 5 or 6 )


This works by implicitly converting Any to Equalizer, which contains the methods above. The code can be found below.

import org.testng.Assert._

case object Equalizer {
implicit def anyToCompare(a: Any) = new Equalizer(a)
}

class Equalizer(a: Any) {

def mustBe(bs: Any*): Unit = {
bs(0) match {
case x:MyTuple => x mustBe a
case _ => {
val message = "In Equalizer: expecting one of=" + bs + "\nIn Equalizer: actual =" + a
println(message)
bs size match {
case 1 => assertEquals(a, bs(0), message)
case _ => assertTrue(bs.contains(a), message)
}
}
}
}

def canBeNullOr(bs: Any*) = {
if (a != null) mustBe(bs: _*)
else println("In Equalizer: expecting one of=" + bs + " or null\nIn Equalizer: actual =" + a)
}

def cantBe(b: Any) = assertFalse(a == b)

def is(b: Any) = a equals b

def or(b: Any) = {
a match {
case x:MyTuple => x + b
case _ => MyTuple2(a, b)
}
}

import Equalizer._

trait MyTuple{
def +(b: Any): MyTuple
def mustBe(a: Any): Unit
}
case class MyTuple2(y: Any, z: Any) extends MyTuple{
def +(b: Any): MyTuple = MyTuple3(y, z, b)
def mustBe(a: Any): Unit = a mustBe (y,z)
}
case class MyTuple3(x: Any, y: Any, z: Any) extends MyTuple{
def +(b: Any): MyTuple = MyTuple4(x, y, z, b)
def mustBe(a: Any): Unit = a mustBe (x,y,z)
}
case class MyTuple4(w: Any, x: Any, y: Any, z: Any) extends MyTuple{
def +(b: Any): MyTuple = MyTuple5(w, x, y, z, b)
def mustBe(a: Any): Unit = a mustBe (w,x,y,z)
}
case class MyTuple5(v: Any, w: Any, x: Any, y: Any, z: Any) extends MyTuple{
def +(b: Any): MyTuple = MyTuple6(v, w, x, y, z, b)
def mustBe(a: Any): Unit = a mustBe (v,w,x,y,z)
}
case class MyTuple6(u: Any, v: Any, w: Any, x: Any, y: Any, z: Any) extends MyTuple{
def +(b: Any): MyTuple = throw new IllegalArgumentException("too many ors")
def mustBe(a: Any): Unit = a mustBe (u,v,w,x,y,z)
}
}


Sorry about MyTuple...I wanted people to be able to use Tuples as arguments in their mustBe statements, so I had to make sure I didn't pass in a Tuple into mustBe, via the "or" method. Anyway, Tuples don't even appear to be working so....ugh... If anyone can help me clean this up, that would be awesome.

Regardless, what do you think? I really like using the code in tests, even if the Equalizer class itself is a bit hairy.

6 comments:

  1. I don't quite get the need for the MyTuple's (didn't think hard about it anyway :) but I like the idea of this DSL.

    ReplyDelete
  2. Here's one way to describe it.

    Lets say the expression x or y returns a Tuple2 - (x,y).

    Now lets x or y is used in an assertion:

    val x = 5
    val y = 6
    val z = 2 + 3

    z must be ( x or y )

    The last statement becomes:

    z must be ( (x,y) )

    In the mustBe method, I need to break apart the Tuple, and check to see if z is either x or y.

    But, what if I really wanted to check that z was a Tuple? Example:

    val z = (5,6)
    z mustBe ( (x,y) )

    What happens here? How do I know that I should't break apart the Tuple and check z against each value in it? With regular Tuples returned from "or" statements, there is no way to know. Hence MyTuple. If I see a regular Tuple, I treat it as a regular value. If I see a MyTuple, I know to check each value.

    All that said....Maybe the ugliness is just a failure in the API, or my inability to see a better solution. Maybe, now that you know the problem, you have some ideas?

    ReplyDelete
  3. Hi Jack,

    > I probably could have used "in" here, such as x mustBe in( 3, 4, 5 ) ... what do you think?

    With specs (latest version), I would indeed write:

    1 must be oneOf(5, 2, 3)

    and this:

    > y canBeNullOr ( 5 or 6 )

    would be (with Strings because this doesn't typecheck with Ints):

    n must be(null) or be oneOf("a", "b")

    which is a bit more verbose.

    Cheers,

    Eric.

    ReplyDelete
  4. Hi Eric,

    Yes, your matchers are generally much better. This was me playing around a bit 6-8 months ago. However, just a question, can you handle the simple case of:

    x must_be 5

    without having to put parens?

    x must be(5)

    I think I prefer the former over the latter.

    BTW I'm giving a presentation on Scala and Testing in NYC at the end of the month. We should catch up. I'd like to talk about specs quite a bit.

    ReplyDelete
  5. No, there's no way to leave out the parenthesis.

    This is why you can write x mustBe 5 in specs, among other common aliases, like must_==.

    > We should catch up. I'd like to talk about specs quite a bit.

    Tell me how I can help you for this. You may also be interested in the latest developments for Acceptance specifications/testing: http://code.google.com/p/specs/wiki/ChangeLog.

    ReplyDelete
  6. mustBe is good. I don't think Bill included it in ScalaTest matchers and I'm going to try to nudge him to do so. However, since doing time with Ruby, I like must_be better.

    I'll get back to you on the other thing, and I'll check out Acc Specs too.

    ReplyDelete