Wednesday, January 23, 2008

Scala and TestNG in Far Greater Detail

I wrote before on running TestNG in Scala and due to popular demand I'm going to go into much greater detail. My goal is to show that running TestNG in Scala is as easy as it is in Java. I've thrown in Hamcrest to show that that integrates seamlessly as well. Hopefully along the way you'll learn a couple of Scala nuggets too. And, be warned I'm assuming you know a bit about TestNG so I'm not going to explain it much. If you don't...www.testng.org

I created a new Scala class for testing my AndGate class. The idea is that I want to make sure that my AndGate is on or off according to the standard And boolean logic table:

x y | output
------------
0 0 | 0
0 1 | 0
1 0 | 0
1 1 | 1

The testing class is called ScalaTestNGExampleTest, and it looks like this:


import org.testng.annotations._
import org.hamcrest.MatcherAssert._
import org.hamcrest.Matchers._;
import org.testng.annotations.DataProvider;
import com.joshcough.cpu.gates._

class ScalaTestNGExample {

@DataProvider{val name="generators"}
def createGenerators = {
val gens = Array(off, on)
for( x <- gens; y <- gens ) yield Array(x,y)
}

private def on = new Generator(true)
private def off = new Generator(false)


@Test{ val dataProvider="generators" }
def testAndGateStates(genA: Generator, genB: Generator){
val and: AndGate = new AndGate(genA, genB);
val whatItShouldBe = genA.on && genB.on
assertThat( and.on, is(whatItShouldBe) );
println( and.on + "==" + genA.on + "&&" + genB.on )
}

@BeforeMethod def printLineBefore = println("------entering test------")
@AfterMethod def printLineAfter = println("------exiting test------")

}

If you aren't familiar with Scala that might look a bit like magic, so I'll explain one step at a time.

Data Provider

The first thing I did was set up a data provider for my logic table:

  1. The annotation declaration is obviously different than in Java. Instead of parentheses, you have to use curly brackets. Instead of simply name/value pairs, you have to declare vars. I could explain why, but instead you could just go to http://www.scala-lang.org/intro/annotations.html.

  2. DataProvider methods are supposed to return Object[][], but...what the heck is this one returning? Well, before I explain what that funky for statement is actually doing I'll just announce that this method is actually returning Array[Array[Generator]]. I could have made it more explicit by saying def createGenerators(): Array[Array[Generator]] but Scala's type inference lets me get away with leaving it off. Does leaving it off hamper readability? In most leaving it off is just eliminating some redundancy. Maybe in this case I should have left it on, but I wanted to show type inference a little bit.

  3. Wait a second...Array[Array[Generator]] isn't Object[][]...or is it? Actually, yes. Scala's typed array class (Array[T]) actually compiles down to Java arrays. In this case, Array[Array[Generator]] compiles down to Generator[][] in Java.

  4. What is Array(on, off)? The type of Array(on, off) is Array[Generator] and it contains two elements, Generator(true) and Generator(false) which are returned from the on and off methods respectively. It may be confusing for a Java programmer to see simply "on" with no parens. In most cases (for reasons far beyond the scope of this post) Scala doesn't force you to use parens on method calls with no arguments.

  5. Ok finally, what is that funky for loop looking thing doing? Rather than explain, why don't I just give the equivalent code in Java for the createGenerators method?


public Generator[][] createGenerators() {

Generator[] onAndOff = new Generator[]{ on(), off() };

Generator[][] gensToReturn = new Generator[4][2];
for( int i = 0; i<onAndOff.length; i++ ){
for( int j = 0; i<onAndOff.length; j++ ){
gensToReturn[i+j] = new Generator[]{ onAndOff[i], onAndOff[j] };
}
}
return gensToReturn;
}

Honestly? Those three lines of code are doing all of that...? Yes. Honestly. Rather than me trying to explain it though, James Iry does an excellent job in part 2 of his four part series on monads called Monads are Elephants. That is the link to part two, but I recommend reading all four.

So now we have a data provider and we have a reasonable idea how it works. But quickly before I move on, heres another way I could have done it which is arguably more readable but not nearly as much fun. Once again, the idea here is that this data provider is essentially creating the And logic table for us.


def createGenerators = {
Array(Array(on, on),Array(on, off),Array(off, on),Array(off, off))
}

Test Method

At this point I think everything else is really straight forward. Despite that, I'll go over the test method in detail.
  1. The @Test annotation is defined in the same fashion as I described above for @DataProvider. In this case I define which data provider to use. Done.

  2. The method takes two Generator parameters which come from createGenerators method.

  3. The first line simply instantiates an AndGate object using the two Generator parameters.

  4. The second and third lines simply assert that the AndGate is what it should be! It should be on only when both generators are on, according to the logic table. The third line uses Hamcrest matchers for asserting. I'm not going to bother explaining them here.

  5. Finally I throw in a print statement.


BeforeMethod And AfterMethod Annotations

Before each test method is called, TestNG will call any methods annotated with @BeforeMethod. In my case before each method I just print a nice message. The same goes for @AfterMethod, but after each test method, of course.

Results

Here is my output from the console:

[testng] -----------entering test-----------
[testng] true==true&&true
[testng] -----------exiting test-----------
[testng] -----------entering test-----------
[testng] false==true&&false
[testng] -----------exiting test-----------
[testng] -----------entering test-----------
[testng] false==false&&true
[testng] -----------exiting test-----------
[testng] -----------entering test-----------
[testng] false==false&&false
[testng] -----------exiting test-----------

Problems

I did run into a few problems.

  1. FIXED: Unfortunately, I couldn't use @Test{expectedExceptions = {SomeException.class}} because Scala doesn't you say Anything.class.
  2. Running the tests through Eclipse is not as easy as it is in Java, and needs some work. I ended up mostly running through Ant, but sometimes through Eclipse.

Conclusion

I hope I've convinced you that running TestNG in Scala is simple. I didn't test all the features, but most of what I have tested works great. I plan to use it for most of my Scala development. I personally think its pretty far ahead of the pack, but if you want to see for yourself they are: ScalaTest, ScUnit, Rehersal, JUnit, and specs. I've tried the last four, and of those I thought specs was really nice. It seems like a reasonable alternative.

I am very open to hear ideas on why I should switch to an xUnit test framework built in Scala. Are there any advantages? What are they?

3 comments:

  1. Can you not use classOf[Exception] when you expect an exception?

    I maintain a Java BDD framework called Instinct, that I've toyed around with adding a Scala layer onto. I had some initial issues trying to get the IntelliJ JUnit runner working but don't foresee any big deals in making it work. Specs and rehearsal look interesting too. An Instinct layer should be pretty nice, it just needs to be written! :)

    ReplyDelete
  2. Thanks.

    That worked:


    @Test{ val expectedExceptions = Array( classOf[Exception] ) }
    def simpleExceptionTest = {
    throw new Exception();
    }

    ReplyDelete
  3. Nice. I was just curious if Scala can work with TestNG and Hamcrest, and BAM here it is. Great work. Thanks

    ReplyDelete