How To Create Temporary Directory in Scala Unit Tests
Asked Answered
R

3

12

In scala how can a unit test create a temporary directory to use as part of the testing?

I am trying to unit test a class which depends on a directory

class UsesDirectory(directory : java.io.File) {
  ...
}

I'm looking for something of the form:

class UsesDirectorySpec extends FlatSpec {
  val tempDir = ??? //missing piece

  val usesDirectory = UsesDirectory(tempDir)

  "UsesDirectory" should {
    ...
  }
}

Also, any comments/suggestions on appropriately cleaning up the resource after the unit testing is completed would be helpful.

Thank you in advance for your consideration and response.

Rusk answered 1/3, 2019 at 14:22 Comment(1)
Maybe this would help?Snelling
C
13

Krzysztof's answer provides a good strategy for avoiding the need for temp directories in your tests altogether.

However if you do need UsesDirectory to work with real files, you can do something like the following to create a temporary directory:

import java.nio.file.Files
val tempDir = Files.createTempDirectory("some-prefix").toFile

Regarding cleanup, you could use the JVM shutdown hook mechanism to delete your temp files.

(java.io.File does provide deleteOnExit() method but it doesn't work on non-empty directories)

You could implement a custom shutdown hook using sys.addShutdownHook {}, and use Files.walk or Files.walkTree to delete the contents of your temp directory.

Also you may want to take a look at the better-files library, which provides a less verbose scala API for common files operations including File.newTemporaryDirectory() and file.walk()

Chlo answered 3/3, 2019 at 14:41 Comment(0)
T
3

File in Java is very cumbersome to test. There is no simple way to create some kind of virtual filesystem abstraction, which can be used for tests.

A cool way around it is to create some kind of wrapper, that can be used for stubbing and mocking.

For example:

trait FileOps { //trait which works as proxy for file
  def getName(): String
  def exists(): Boolean
}

object FileOps {

  class FileOpsImpl(file: File) extends FileOps {
    override def getName(): String = file.getName //delegate all methods you need
    override def exists(): Boolean = file.exists()
  }

  implicit class FromFile(file: File) { //implicit method to convert File to FileOps
    def toFileOps: FileOpsImpl = new FileOpsImpl(file)
  }
}

Then you'd have to use it instead of File in your class:

class UsesDirectory(directory : FileOps) {
  ...
}

//maybe you can even create implicit conversion, but it's better to do it explicitly
val directory = new UserDirectory(file.toFileOps) 

And what is benefit of that?

In your tests you can provide custom implementation of FileOps:

class UsesDirectorySpec extends FlatSpec {
    val dummyFileOps = new FileOps {
        override def getName(): String = "mock"
        override def exists(): Boolean = true
    }

    //OR

    val mockFileOps = mock[FileOps] //you can mock it easily since it's only trait

    val usesDirectory = UsesDirectory(dummyFileOps)

    "UsesDirectory" should {
      ...
    }
}

If you use this or a similar approach, you don't even need to touch filesystem in your unit test.

Technology answered 2/3, 2019 at 9:59 Comment(0)
G
2

I came across a similar requirement to unit test my functions against sample files. I solved it using scalatest.BeforeAndAfterAll trait available in the scalatest package.

import com.nag.SensorApp
import com.nag.repository.SensorStats
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.{BeforeAndAfterAll}

import java.io.File
import java.io.FileWriter
import java.nio.file.{Files, Path, Paths}
import scala.collection.mutable


class AggregatorServiceTests extends AnyFlatSpec with BeforeAndAfterAll {

  val testFileName = "test-files"
  val configMap = mutable.Map.empty[String, File]

Next we override the beforeAll() method, which can setup the tempFile before the tests. The corresponding afterAll() will ensure to cleanup these temporary files after the tests have been executed.


  override def beforeAll() = {
    val tempDir = Files.createTempDirectory(testFileName)
    print("The temporary directory = " +(tempDir))
    val tempFile = new File(osAwareFilename(tempDir, testFileName))
    tempFile.createNewFile()
    configMap.put(testFileName, tempFile)
    val myWriter = new FileWriter(tempFile)
    myWriter.write("sensor-id,humidity\ns1,10\ns2,88\ns1,NaN")
    myWriter.close()
  }

  override def afterAll(): Unit = {
    super.afterAll()
    val tempFile = configMap.get(testFileName).get
    tempFile.delete()
  }

  private def osAwareFilename(inputPath : Path, filename : String) : String = {
    if (System.getProperty("os.name").contains("Windows")) {
      inputPath.toFile.getPath + "\\" + filename
    }
    else {
      inputPath.toFile.getPath + "/" + filename
    }
  }

Once this setup is done, the tempFile can be accessed from the configMap and used in our tests.

  "The Aggregator Service" should "sum sensor s1 and s2 records ignoring NaN " in  {
    val fileForTest = configMap.get(testFileName).get
    println("File for test :: "+ fileForTest.toPath.getParent)
    val aggregatorService = SensorApp.main(fileForTest.toPath.getParent)
    assert(SensorStats.totalRowsProcessed == 3)
    assert(SensorStats.totalFailedMeasurements == 1)
    assert(SensorStats.invalidSensors.keys.size == 0)
    assert(SensorStats.sensorInfo.get("s1").get.sum == 10)

  }

}
Gynarchy answered 31/3, 2023 at 20:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.