Java how to test file utility that are void methods?

How exactly would I test classes or methods that involve writing out files, or moving files around from directory to directory? Let’s say I have this helper method as one of Spring MVC’s Service layer methods:

private void writeFileOut(String fileContents, String fileName) throws IOException {
        File fullFilePath;
        FileWriter fileWriter = null;
        BufferedWriter bufferedWriter = null;
        try {
            fullFilePath = new File("/temp/directory/" + fileName);
            fileWriter = new FileWriter(fullFilePath);
            bufferedWriter = new BufferedWriter(fileWriter);
            if (bufferedWriter != null) {
                bufferedWriter.append(fileContents);
            }
        } catch (IOException e) {
            LOGGER.info("Error writing file out: " + e.getMessage());
            throw e;
        } finally {
            try {
                bufferedWriter.close();
            } catch (IOException e) {
                throw e;
            }
        }
    }

How exactly is this method testable? It isn’t producing anything and I can’t think of a way to test this is working exactly.

Answer

First things first: why reinvent the wheel instead of using Files.writeString?

Assuming you are looking for a more general solution to test code that touches the file system: I’d try to keep any external resource (network, database, other processes, file system) out of my unit tests. You’ll end up with brittle tests that depend on e.g., file system details (latency, cleanup between tests, tests running in parallel may step on each others toes).

Then: please use try with resources:

private void writeFileOut(String fileContents, String fileName) throws IOException {
    File fullFilePath = new File("/temp/directory/" + fileName);
    try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(fullFilePath))) {
        bufferedWriter.append(fileContents);
    }
    catch (IOException e) {
        LOGGER.info("Error writing file out: " + e.getMessage());
        throw e;
    }
}

Now your code does 3 things:

  • create the path for the file
  • write the given data to the file
  • log if an error occurs

It’s often easiest to move code you want to test into new methods that you can call separately, instead of having to test through all the other code. So isolate pieces of code, especially the code that tries to use the file system.

private void writeFileOut(String fileContents, String fileName) throws IOException {
    File fullFilePath = new File(makeTempPath(fileName));
    try (Writer bufferedWriter = makeWriter(fullFilePath)) {
        bufferedWriter.append(fileContents);
    }
    catch (IOException e) {
        LOGGER.info("Error writing file out: " + e.getMessage());
        throw e;
    }
}

String makeTempPath(String fileName) {
    return "/temp/directory/" + fileName;
}

Writer makeWriter(String fullFilePath) {
    return new BufferedWriter(new FileWriter(fullFilePath));
}

Now you can test makeTempPath separately.

You can mock makeWriter when you test writeFileOut. You can check that it receives what it was supposed to receive. You can have it throw to trigger the error handling.

When you mock, you can use a framework like Mockito or you create a derived class for the methods you want to mock and override them. Note that makeWriter returns a Writer. In real life this is the BufferedWriter that writes to a file. In testing you can return a StringWriter to capture what gets written.

Either way, be careful not to mock too much, or you may end up just testing your mocks, not the production code.