Wednesday, September 29, 2010

BDD and ScalaTest

I've read a good bit about ScalaTest and BDD before, but I actually used it for the first time on a particular project I'm working on at the moment.

The code in question was a bit of utility code that simply extracts a zip archive or archive's a directory (a basic packaging utility). Writing a spec for this was very easy.

  • Given an archive, extracting it should produce the known directory structure.
  • The extracted contents should be the same as the known contents
  • Archiving should produce an archive of directory
  • Archive should be valid (ie: no errors on extracting)

That's four quick ones. Below, I have the spec written out as a ScalaTest. I couldn't help but to have a big, goofy smile when I ran it through sbt.

package test

import org.scalatest.{FlatSpec, BeforeAndAfterAll}
import org.scalatest.matchers.ShouldMatchers
import Zip._

class ZipSpec extends FlatSpec with ShouldMatchers with BeforeAndAfterAll {
  import java.io.File
  val archivePath = getClass.getClassLoader.getResource("archive.zip")

  override def afterAll(configMap: Map[String, Any]) {
    def recurse(file: File)(fun: File => Unit) {
      if(file.isDirectory) 
        file.listFiles.filter(f => !f.getName.startsWith(".")).foreach {
          recurse(_)(fun)
        }
      fun(file)
    }

    // Delete temp files
    recurse(new File("archive")) { _.delete }
    recurse(new File("temp")) { _.delete }
    new File("../archive.zip").delete
    new File("archive.zip").delete
  }

  "Test archive" should "exists" in {
    val archive = new File(archivePath.getFile)
    archive should not be (null)
  }  

  "Extract" should """create directory tree: 
  archive/ 
  archive/child/ 
  archive/child/more.txt 
  archive/test.xml""" in {
    extract(archivePath.getFile)

    // Checking Dir tree
    new File("archive") should be ('exists)
    new File("archive/test.xml") should be ('exists)
    new File("archive/child") should be ('exists)
    new File("archive/child/more.txt") should be ('exists)
  }

  it should """create a new directory tree:
  temp/archive
  temp/archive/child/
  temp/archive/child/more.txt
  temp/archive/test.xml""" in {
    extract(archivePath.getFile, "temp")

    new File("temp/archive") should be ('exists)
    new File("temp/archive/test.xml") should be ('exists)
    new File("temp/archive/child") should be ('exists)
    new File("temp/archive/child/more.txt") should be ('exists)
  }

  "Extracted flat files" should "contain correct data" in {
    import scala.xml._
    import scala.io.Source.{fromFile => open}

    val xmltext = <stuff>
  <more-stuff>Test</more-stuff>
</stuff>
    val text = "You'll never find me!"

    XML.loadFile("archive/test.xml") should be === xmltext
    open("archive/child/more.txt").getLines.mkString should be === text
  }

  "Archive" should "create archive.zip" in {
    archive("archive")

    new File("archive.zip") should be ('exists)
  }

  it should "create archive.zip in parent directory" in {
    archive("archive", "../")

    new File("../archive.zip") should be ('exists)
  }

  "../archive.zip and archive.zip" should "be the same size" in {
    val parentzip = new File("../archive.zip")
    val archivezip = new File("archive.zip")

    parentzip.length should be === archivezip.length
  }

  "Extracting archive.zip" should "not throw any exceptions" in {
    extract("archive.zip")
  }
}

Here's example output from sbt's execution of the test spec.

[info] == test.ZipSpec ==
[info] Test archive
[info] Test Starting: Test archive should exists
[info] Test Passed: Test archive should exists
[info] Extract
[info] Test Starting: Extract should create directory tree:
[info]   archive/
[info]   archive/child/
[info]   archive/child/more.txt
[info]   archive/test.xml
[info] Test Passed: Extract should create directory tree:
[info]   archive/
[info]   archive/child/
[info]   archive/child/more.txt
[info]   archive/test.xml
[info] Test Starting: Extract should create a new directory tree:
[info]   temp/archive
[info]   temp/archive/child/
[info]   temp/archive/child/more.txt
[info]   temp/archive/test.xml
[info] Test Passed: Extract should create a new directory tree:
[info]   temp/archive
[info]   temp/archive/child/
[info]   temp/archive/child/more.txt
[info]   temp/archive/test.xml
[info] Extracted flat files
[info] Test Starting: Extracted flat files should contain correct data
[info] Test Passed: Extracted flat files should contain correct data
[info] Archive
[info] Test Starting: Archive should create archive.zip
[info] Test Passed: Archive should create archive.zip
[info] Test Starting: Archive should create archive.zip in parent directory
[info] Test Passed: Archive should create archive.zip in parent directory
[info] ../archive.zip and archive.zip
[info] Test Starting: ../archive.zip and archive.zip should be the same size
[info] Test Passed: ../archive.zip and archive.zip should be the same size
[info] Extracting archive.zip
[info] Test Starting: Extracting archive.zip should not throw any exceptions
[info] Test Passed: Extracting archive.zip should not throw any exceptions
[info] == test.ZipSpec ==
[info]
[info] == test-complete ==
[info] == test-complete ==
[info]
[info] == test-finish ==
[info] Passed: : Total 8, Failed 0, Errors 0, Passed 8, Skipped 0
[info]
[info] All tests PASSED.
[info] == test-finish ==

As inevitably a part of testing an archive utility, the test code will create a bunch of files we don't want. Fortunately for us, ScalaTest comes with a BeforeAndAfterAll trait for us. I just to needed override afterAll in this case, where I put the code to remove what was created.

I feel the immediate advantage of this test framework is that the test code itself quite literally explains what's going on. Once I got the spec written, refactoring the implementation code was breezy (which is characteristic of unit testing in general).

Consider me a ScalaTest convert. I doubt I'll ever write a traditional unit test again.

-- Philip

No comments:

Post a Comment