Friday, April 9, 2010

First Adventures in ScalaTest

As mentioned earlier I'm spending some time learning Scala. To that end I've read Programming Scala - which BTW is a book I'd recommend to anyone looking into Scala. Towards the end of the book there's an example of a litte application that can calculate the net worth of some stocks: Given a collection of ticker symbols and amounts it goes to Yahoo and finds the latests trading prices and adds up the net worth. Not too complicated really. I'm sort of following along with the book in my own code, but I'm also sort of straying. In later posts I'll show the actual application, but for know I just want to show some of the tests I've written along the way.


I'm using ScalaTest for testing which comes in a couple of flavors of which I've chosen the BDD styled FlatSpec style. And I'm really enjoying it. Let's look at some of the things I like about it. First, here are just two simple tests for the constructor of my code under test:


class PortfolioManagerSpec extends FlatSpec with ShouldMatchers {
  def withFakeStockPriceFinder(
        fileName: String)
       (testFunctionBody: (PortfolioManager) => Unit) {
    testFunctionBody(new PortfolioManager(fileName)
      with FakeStockPriceFinder)
  }


  "A PortfolioManager" should "be instantiable with a valid file name" in {
    withFakeStockPriceFinder("src/configuration/stocks.xml")
     {pm =>}
  }


  it should "throw an exception when given an invalid file name" in {
    evaluating {
      withFakeStockPriceFinder("fakefilename") {pm =>}
    } should produce[FileNotFoundException]
  }
  ...
}


The ScalaTest flavor is chosen simply by extending one of several traits, in this case FlatSpec. FlatSpec gives me the ability to specify test cases in " should in" fashion as seen in the first test above. To me doing that in a statically typed language is awesome, and goes to show how flexible Scala really is. The second test starts with "it", which means that it continues on the line of testing of the test above. In this case "it" lets me avoid repeating the "A PortFolioManager" part.


Even these simple tests already puts Scalas strong support for functions into play: The withFakeStockPriceFinder method at the top is a curried method that takes first a string argument, and then a function argument, testFunctionBody. The method instantiates the object under test and passes it into the testFunctionBody. Both test cases above use withFakeStockPriceFinder to contruct the object under test, and have it passed into an anonymous function where the actual test code is written. Furthermore the second test makes use of some of the things from the ShouldMatchers trait, namely the evaluating, should and produce methods. Evaluating takes a function argument and executes it, much like my own withFakeStockPriceFinder, but it handles any exceptions thrown and lets me set up expectations for exceptions by calling the should and produce methods in a fluent fashion. -Note that dots and parenthesis in method calls are optional in Scala, as long as there are no ambiguities. This is IMHO is also awesome, and again goes to show how flexible Scala is.


Before showing the rest of my PortfolioManagerSpec I want to touch on another point: TDD is - as we know - a very disciplined way of working, and can be hard to follow all the time but somehow I find that the style of testing promoted by the FlatSpec and ShouldMatchers nudges me towards shorter red-green cycles and a more stringent test-first practice, than I usually have with NUnit. I'm not sure why that is, but it has to do with the way tests are declared with strings rather that method names, I think.


Anyway here's the whole PortfolioManagerSpec:


class PortfolioManagerSpec extends FlatSpec with ShouldMatchers {
  def withFakeStockPriceFinder(
        fileName: String)
       (testFunctionBody: (PortfolioManager) => Unit) {
    testFunctionBody(new PortfolioManager(fileName)
      with FakeStockPriceFinder)
  }


  "A PortfolioManager" should "be instantiable with a valid file name" in {
    withFakeStockPriceFinder("src/configuration/stocks.xml") 
      {pm =>}
  }


  it should "throw an exception when given an invalid file name" in {
    evaluating {
      withFakeStockPriceFinder("fakefilename") {pm =>}
    } should produce[FileNotFoundException]
  }


  "A PortfolioManager for stocks.xml" should "find 15 symbols and units in the stocks.xml file" in {
    withFakeStockPriceFinder("src/configuration/stocks.xml") {
      pm =>
        val tickersAndUnits = pm.tickersAndUnits
        tickersAndUnits.size should be (15)
    }
  }


  it should "find APPL, NSM and XRX ticker symbols" in {
    withFakeStockPriceFinder("src/configuration/stocks.xml") {
      pm =>
        val tickersAndUnits = pm.tickersAndUnits
        tickersAndUnits should (contain key ("APPL") and contain key ("NSM") and contain key ("XRX"))
    }
  }


  it should "find 200 ORCL" in {
    withFakeStockPriceFinder("src/configuration/stocks.xml") {
      pm =>
        val tickersAndUnits = pm.tickersAndUnits
        tickersAndUnits("ORCL") should be (200)
    }
  }


  it should "produce a asset report and calculate the total net worth" in {
    withFakeStockPriceFinder("src/configuration/stocks.xml") {
      pm =>
        val (totalNetWorth, report) =                  
          pm.generateTotalNetWorthAndSimpleReport
        totalNetWorth should be (146250)

        print(report)
    }
  }
}

2 comments: