In this article we’ll go through the exercise of writing a method that will write string content to the specified file. There will be an option to specify whether we should overwrite an existing file. In addition, directories should be created if they do not already exist.
Programming language is Scala and testing framework that will be used is Specs2. In the spirit of unit testing, instead of interactions with the file system we’ll use mocks with Mockito (already included in Specs2). All the code will be done using Test-Driven Development (TDD).
This article is based on an existing code done for the open source application BDD Assistant located in the TechnologyConversationsBdd repository.
Let’s get started.
In order to use Specs2 and Mockito, we’ll create a specs class that extends both. The initial class declaration is following:
[BddFileSpec.scala]
class BddFileSpec extends Specification with Mockito { "BddFile#saveFile" should { } }
BddFile#saveFile is the holder of all specifications related to the saveFile method that we’re about to write. The method itself is following:
[BddFile.scala]
def saveFile(file: File, content: String, overwrite: Boolean): Boolean = { true }
At the moment it only returns true. As we progress with the exercise, it will grow to fulfill all specifications.
The first specification should verify that when file exists and should not be overwritten, method should return false.
[BddFileSpec.scala]
val content = "SOME CONTENT" val file = mock[File] // Create a mocked class "return false if file exists and should not be overwritten" in { val bddFile = BddFile() file.exists() returns true // When exists method is called, return value will be always true val actual = bddFile.saveFile(file, content, overwrite = false) actual must beFalse }
Important thing to note about this code is that we are mocking the File class. When classes are mocked, Mockito replaces all their public and protected methods with mocks. Real File class will not be used at all. Further on, we’re stubbing the exists method so that it always returns true. In this way we’re telling methods in the mocked class how to behave. Without mocks specification would run slower due to its need to interact with the file system and, more importantly, we’d need to make sure that the file does exists. In this way we are creating more reliable and faster tests. Keep in mind that we are trying to test a single unit (in this case the method saveFile and trust that all other methods or objects it interacts with are already tested and working as expected.
When this specification is run it should fail. Remember the TDD process: write one test, confirm that it fails, write the implementation, confirm that the test run is successful, refactor if needed, repeat. Small steps that lead towards the complete solution.
Here’s the code that makes the above specification pass.
[BddFile.scala]
def saveFile(file: File, content: String, overwrite: Boolean): Boolean = { if (file.exists && !overwrite) { false } else { false } }
Let’s move to the next specification.
[BddFileSpec.scala]
"create parent dir when specified" in { val bddFile = BddFile() val parentDir = mock[File] file.exists() returns false file.getParentFile returns parentDir // Return mocked File parentDir when getParentFile is called bddFile.saveFile(file, content, overwrite = false) there was one(parentDir).mkdirs() // Verify that mkdirs was called exactly one time. }
In this specification we are telling the mocked File class to return another mock declared as parentDir when method getParentFile is called. Further on, we are verifying that mkdirs was called exactly one time.
This implementation code is following.
[BddFile.scala]
def saveFile(file: File, content: String, overwrite: Boolean): Boolean = { if (file.exists && !overwrite) { false } else { val parentDir = file.getParentFile parentDir.mkdirs() false } }
Let’s move to the next specification.
[BddFileSpec.scala]
"NOT create parent dir if NOT specified" in { val bddFile = BddFile() val parentDir = mock[File] file.exists() returns false file.getParentFile returns null bddFile.saveFile(file, content, overwrite = false) there was no(parentDir).mkdirs() // Verify that mkdirs was not called }
In the similar fashion as the previous specification, this time we’re verifying that mkdirs was not called.
Implementation code is following.
[BddFile.scala]
def saveFile(file: File, content: String, overwrite: Boolean): Boolean = { if (file.exists && !overwrite) { false } else { val parentDir = file.getParentFile if (parentDir != null) { parentDir.mkdirs() } false } }
Off to the next specification.
[BddFileSpec.scala]
"call writeStringToFile" in { val bddFile = spy(BddFile()) doNothing().when(bddFile).writeStringToFile(any[File], anyString) // Make sure that the real writeStringToFile method is never called. file.exists() returns true bddFile.saveFile(file, content, overwrite = true) there was one(bddFile).writeStringToFile(file, content) // Verify that writeStringToFile was called exactly one time }
We’ll use FileUtils from Apache Commons IO to write the content to the file. However, FileUtils.writeStringToFile is a static method that cannot be easily mocked with Mockito. To avoid the real method being called, we’ll move it to the separate method writeStringToFile. Alternative to this approach would be to use PowerMock that has more options when compared to Mockito.
This specification introduces a concept of spies. Unlike mocks that replace all methods with fake ones, spies replace only those that we specify. In this case, we’re telling the spied BddFile to do nothing when writeStringToFile is called. Later on we’re verifying that the method writeStringToFile is called exactly one time.
The implementation code is following.
[BddFile.scala]
def saveFile(file: File, content: String, overwrite: Boolean): Boolean = { if (file.exists && !overwrite) { false } else { val parentDir = file.getParentFile if (parentDir != null) { parentDir.mkdirs() } writeStringToFile(file, content) false } } private[file] def writeStringToFile(file: File, content: String): Unit = { FileUtils.writeStringToFile(file, content, "UTF-8") }
Since the introduction of the writeStringToFile method, our previous specifications will fail. To fix that, we’ll need to go back and create spies in all but the first one. You’ll be able to see the full code at the end of the article.
We are almost done. All that is left is to make sure that our method returns true whenever file was written. To do that, we’ll write two additional specifications.
[BddFileSpec.scala]
"return true when file does not exist" in { val bddFile = spy(BddFile()) doNothing().when(bddFile).writeStringToFile(any[File], anyString) file.exists() returns false val actual = bddFile.saveFile(file, content, overwrite = false) actual must beTrue } "return true when file exist but should be overwritten" in { val bddFile = spy(BddFile()) doNothing().when(bddFile).writeStringToFile(any[File], anyString) file.exists() returns true val actual = bddFile.saveFile(file, content, overwrite = true) actual must beTrue }
The implementation is easy and requires us to change the last false to true.
[BddFile.scala]
def saveFile(file: File, content: String, overwrite: Boolean): Boolean = { if (file.exists && !overwrite) { false } else { val parentDir = file.getParentFile if (parentDir != null) { parentDir.mkdirs() } writeStringToFile(file, content) true } }
That’s it. We have a fully working method that writes contents of a string to a file, creates directories if required and does nothing when overwrite is set to false.
Complete specifications code can be found in BddFileSpec.scala together with the implementation in BddFile.scala. Both are part of the open source project BDD Assistant with the code located in the TechnologyConversationsBdd GitHub repository.