This article may be considered as a continuation of Introduction to Writing Tests with ScalaTest. In this post I would like to talk about ScalaMock.

Mocking is a technique by which you can test code without requiring all of its dependencies in place. Java offers several frameworks for mocking objects when writing tests. The most well known are JMock, EasyMock, and Mockito. As the Scala language introduces new elements such as traits and functions, the Java-based mocking frameworks are not enough, and this is where ScalaMock comes into play.

ScalaMock is a native Scala-mocking framework that is typically used within ScalaTest (or Specs2), by importing the following dependencies into the SBT (build.sbt) file:

libraryDependencies +="org.scalamock" %% "scalamock-scalatest-support" % "3.0.1" % "test"

Within Specs2, the following dependencies need to be imported:

libraryDependencies +=
"org.scalamock" %% "scalamock-specs2-support" % "3.0.1" % "test"

Since the release of the Scala Version 2.10, ScalaMock has been rewritten, and the ScalaMock Version 3.x is the version that we are going to cover briefly by going through an example of mocking a trait.

Let's first define the code that we are going to test. It consists of a tiny currency converter (available at http://www.luketebbs.com/?p=58) that fetches currency rates from the European Central Bank. Retrieving and parsing the XML file of currency rates is only a matter of a few lines of code, as follows:

trait Currency {
  lazy val rates : Map[String,BigDecimal] = {
  val exchangeRates =
    "http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"
  for (
    elem <- xml.XML.load(exchangeRates)\"Cube"\"Cube"\"Cube")
  yield
    (elem\"@currency").text -> BigDecimal((elem\"@rate").text)
  }.toMap ++ Map[String,BigDecimal]("EUR" -> 1)

  def convert(amount:BigDecimal,from:String,to:String) =
    amount / rates(from) * rates(to)
}

In this example, the currency rates are fetched from a URL using the xml.XML.load method. As XML is part of the Scala standard library, there is no need for imports here. The load method parses and returns the XML rates as an immutable structure of type Elem, which is a case class that represents XML elements. This is shown in the following code:


  Reference rates
    
      European Central Bank
    
    
      
      
      
      
      
      
      
           ...
         ...
    
  

Accessing the list of currency rates from this XML document is done through an XPath expression by navigating inside the Cube nodes, hence the xml.XML.load(exchangeRates) \ «Cube» \ «Cube» \ «Cube» expression. A single for comprehension is required to loop over the currency rates and return a collection of key -> value pairs where, in our case, a key will be a string that represents the currency name, and value will be a BigDecimal value that represents the rate. Notice how the information is extracted from by writing (elem \ "@currency").text to capture the currency attribute and (elem \ "@rate").text to capture the rate respectively. The latter will be further processed by creating a new BigDecimal value from the given string.

In the end, we get a Map[String, BigDecimal] that contains all our currencies with their rates. To this value, we add the mapping for the currency EUR (Euros) that will represent the reference rate one; this is why we use the ++ operator to merge two maps, that is, the one we just created together with a new map containing only one key -> value element, Map[String,BigDecimal](«EUR»-> 1).

Before mocking, let's write a regular test using ScalaTest with FlatSpec and Matchers. We will make use of our Converter trait, by integrating it into the following MoneyService class:

package kkrk.st

class MoneyService(converter:Converter ) {

  def sendMoneyToSweden(amount:BigDecimal,from:String): BigDecimal = {
    val convertedAmount = converter.convert(amount,from,"SEK")
    println(s" $convertedAmount SEK are on their way...")
    convertedAmount
  }

  def sendMoneyToSwedenViaEngland(amount:BigDecimal,from:String): BigDecimal = {
    val englishAmount = converter.convert(amount,from,"GBP")
    println(s" $englishAmount GBP are on their way...")
    val swedishAmount = converter.convert(englishAmount,"GBP","SEK")
    println(s" $swedishAmount SEK are on their way...")
    swedishAmount
  }
}

A possible test specification derived from the MoneyService class is as follows:

package kkrk.st

import org.scalatest._
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner

@RunWith(classOf[JUnitRunner])
class MoneyServiceTest extends FlatSpec with Matchers {

"Sending money to Sweden" should "convert into SEK" in {
    val moneyService = 
      new MoneyService(new ECBConverter)
    val amount = 200
    val from = "EUR"
    val result = moneyService.sendMoneyToSweden(amount, from)
    result.toInt should (be > (1700) and be <= (1800))
  }

"Sending money to Sweden via England" should "convert into GBP then SEK" in {
    val moneyService = 
      new MoneyService(new ECBConverter)
    val amount = 200
    val from = "EUR"
    val result = moneyService.sendMoneyToSwedenViaEngland(amount, from)
    result.toInt should (be > (1700) and be <= (1800))
  }
}

To be able to instantiate the Converter trait, we use an ECBConverter class defined in the Converter.scala file as follows:

class ECBConverter extends Converter

If we execute the test from the SBT command prompt or directly within Eclipse (as a JUnit), we get the following output:

> test
[info] Compiling 1 Scala source to /Users/kukuruku/articles/47/HttpSamples/target/scala-2.10/test-classes...
 1792.2600 SEK are on their way...
 167.70000 GBP are on their way...
 1792.2600 SEK are on their way...
[info] MoneyServiceTest:
[info] Sending money to Sweden
[info] - should convert into SEK
[info] Sending money to Sweden via England
[info] - should convert into GBP then SEK
[info] Passed: : Total 2, Failed 0, Errors 0, Passed 2, Skipped 0
[success] Total time: 1 s, completed

If the URL from which we are retrieving the currency rates is not always available, or if the currency rates have changed a lot on one particular day and the resulting amount of the conversion is not in the given interval of the assertion should (be > (1700) and be <= (1800)), then our test might fail. In that case, mocking the converter in our test seems appropriate, and can be done as follows:

package kkrk.st

import org.scalatest._
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalamock.scalatest.MockFactory

@RunWith(classOf[JUnitRunner])
class MockMoneyServiceTest extends FlatSpec with MockFactory with Matchers {

"Sending money to Sweden" should "convert into SEK" in {

    val converter = mock[Converter]
    val moneyService = new MoneyService(converter)

    (converter.convert _).expects(BigDecimal("200"),"EUR","SEK").returning(BigDecimal(1750))

    val amount = 200
    val from = "EUR"
    val result = moneyService.sendMoneyToSweden(amount, from)
    result.toInt should be (1750)
  }
}

The expects method contains the arguments that we expect when our code should invoke the convert method, and the returning method contains our expected output in place of the real return result.

ScalaMock has many variations on how to apply the mocking code, and is planning to enhance the mocking syntax using the Macros in future releases. In short, Macros are functions that are called by the compiler during compilation. It is an experimental feature added in Scala from Version 2.10 that makes it possible for the developer to access the compiler APIs and apply transformations to the AST (Abstract Syntax Tree), that is, the tree representation of a program. I'm not going to talk about Macros right now, but among other things, they are useful for the Code Generation and DSLs. Their usage will improve the ScalaMock syntax; for instance, you can apply your mock expectations within inSequence {… } or the inAnyOrder {… } blocks of code or in nested combinations of these blocks, as illustrated in their documentation, which is available at scalamock.org. ScalaMock also supports a more Mockito-like style with a Record-then-Verify cycle rather than the Expectations-First style, which we have been using.

References

Write your own articles at Kukuruku Hub

0 comments