Dear Junior
As I like the flow of test driven development, it was natural to me to try TDD when starting using Scala. As Scala runs on the JVM and interoperates well with Java it should be easy to just use JUnit. However, it was not totally obvious how to do it, and looking around the net I found no "start from scratch, this is how to do it"-tutorial. Anyway, this is how I did it, starting from scratch.
A First Test - without JUnit
A First Test - without JUnit
Let's start with writing a test for the now-classical Money-example. Using my IDE at hand (IntelliJ), I created a file 'MoneyTest.scala' and wrote the test checking that one swedish krona is one swedish krona.
class MoneyTest {
def should_equal_same_amount_same_currency = {
new Money("SEK", 1) == new Money("SEK", 1)
}
}
Let us pick this apart. This is a class (MoneyTest) that can be used to create instances of that class. Such an instance ("a MoneyTest") is in other words "an object that can test properties of Money". One of the properties it can test is that Money "should equal same amount same currency". Testing the property is done by running the method.
We have not yet got JUnit in the loop, but this is the anatomy of a unit test in place.
Get it to Compile
To get the test through the compiler we need a Money class as well, which will reside in Money.scala.
We have not yet got JUnit in the loop, but this is the anatomy of a unit test in place.
Get it to Compile
To get the test through the compiler we need a Money class as well, which will reside in Money.scala.
class Money(currency : String, amount : Int) {}
Yepp, that's it - class declaration and constructor rolled together.
Now we are in shape having our first unit test and enough implementation to satisfy the compiler. Time to run the test. If we want to we can run it on the interactive read-eval-print-loop (REPL).
scala> new MoneyTest().should_equal_same_amount_same_currency
res0: Boolean = false
Well, this is actually a failing unit test, giving us the permission to hack away.
Taking the Red Test to JUnit
Taking the Red Test to JUnit
However, I have come to be used to JUnit - I like the assert methods, I like how it integrates with my IDE, I like how it integrates with build servers. So, I would like to pimp up this pretty naked unit test to make use of JUnit.
First step was to add JUnit to the Scala project. In IntelliJ I created a new "External Library" named "junit" to which I added junit-4.4.jar - I happened to find it in ~/.m2/repository/junit/junit/4.4/junit-4.4.jar. Then I added the new external library to the project.
Next step is the code. As JUnit is now on the classpath, I can do the imports, annotations, and use the methods I am used to (albeit the import will be Scala-scented).
Next step is the code. As JUnit is now on the classpath, I can do the imports, annotations, and use the methods I am used to (albeit the import will be Scala-scented).
Looks familiar, doesn't it? Now I can run it from within IntelliJ like any JUnit-test. Running it gives another familiar message:import org.junit.Testimport org.junit.Assert._class MoneyTest {@Testdef should_equal_same_amount_same_currency = {assertTrue(new Money("SEK", 1) == new Money("SEK", 1))}}
java.lang.AssertionError:
at org.junit.Assert.fail(Assert.java:74)
at org.junit.Assert.assertTrue(Assert.java:37)
at org.junit.Assert.assertTrue(Assert.java:46)
at MoneyTest.should_equal_same_amount_same_currency(MoneyTest.scala:10)
...
Well, using assertTrue never gave helpful error messages. Let's do better.
Using JUnit
Let us change the assert and use assertEquals instead. This works fine as the mother-class in Scala (named AnyRef) is nothing but "java.lang.Object" so there will be an equals method around.
To be precise, there is actually a class above "AnyRef" named "Any" which include the immutable value-types in Scala that parallel the Java primitives. But that distinction does not matter here.
Changing the code to use assertEquals we get:
Using JUnit
Let us change the assert and use assertEquals instead. This works fine as the mother-class in Scala (named AnyRef) is nothing but "java.lang.Object" so there will be an equals method around.
To be precise, there is actually a class above "AnyRef" named "Any" which include the immutable value-types in Scala that parallel the Java primitives. But that distinction does not matter here.
Changing the code to use assertEquals we get:
@Testdef should_equal_same_amount_same_currency = {assertEquals(new Money("SEK", 1), new Money("SEK", 1))}
This lands me with the error message:
java.lang.AssertionError: expected:<Money@348bdcd2> but was:<Money@4a4e79f1>at ...at MoneyTest.should_equal_same_amount_same_currency(MoneyTest.scala:10)
OK, two Money instances with different hashcodes are obviously not considered equal, even though their data are the same. This might be what we want for entities or domain events, but not for value objects such as Money.
However the error message looks kind of obscure due to the default toString. Let us add a custom toString to get readable messages in the future.
class Money(currency : String, amount : Int) {override def toString() : String = currency + " " + amount}
Running the tests again gives:
java.lang.AssertionError: expected: Money<SEK 1> but was: Money<SEK 1>
at ...
This is definitely more readable. Arguably it would be more confusing had we not remembered the last error message: they are different instances. So, the equality test that Scala gives us from the Scala top class AnyRef (which is the same as java.lang.Object on the JVM) does not cut it for value objects - exactly as in Java.
Fixing Equality
Fixing Equality
What to do? Of course we could get into Money and add an override def equals returning true. Then we could add a testcase to provoke a failing equals, forcing us to elaborate our equals method etc. However, in Scala there is a simpler solution at hand - declare Money as a "case class".
case class Money(currency : String, amount : Int) {override def toString() : String = currency + " " + amount}
Let us not get into the details, but apart from giving us well-behaving equals and hashcode, it actually makes sense for value objects to be case classes.
Green Test, Refactoring, and Ready to Start over again
The good part is that this change take us home to a green bar. There is not much refactoring to do, so now we have gone a full TDD cycle using Scala and JUnit. Time for next round.
Now we have gotten started
Yours