48

I'm pretty new to mocking, and I've been trying to mock the actual contents (essentially create a virtual file in memory alone) so that no data is written to disk at any point.

I've tried solutions like mocking the file and mocking as many of the properties that I can figure out as much as possible, an then also writing into it with a filewriter/bufferedwriter, but those don't work well, since they need canonical paths. Anyone found a solution other than this or similar, but that I'm approaching this wrong?

I've been doing it like this:

private void mocking(){
    File badHTML = mock(File.class);
    //setting the properties of badHTML
    when(badHTML.canExecute()).thenReturn(Boolean.FALSE);
    when(badHTML.canRead()).thenReturn(Boolean.TRUE);
    when(badHTML.canWrite()).thenReturn(Boolean.TRUE);
    when(badHTML.compareTo(badHTML)).thenReturn(Integer.SIZE);
    when(badHTML.delete()).thenReturn(Boolean.FALSE);
    when(badHTML.getFreeSpace()).thenReturn(0l);
    when(badHTML.getName()).thenReturn("bad.html");
    when(badHTML.getParent()).thenReturn(null);
    when(badHTML.getPath()).thenReturn("bad.html");
    when(badHTML.getParentFile()).thenReturn(null);
    when(badHTML.getTotalSpace()).thenReturn(0l);
    when(badHTML.isAbsolute()).thenReturn(Boolean.FALSE);
    when(badHTML.isDirectory()).thenReturn(Boolean.FALSE);
    when(badHTML.isFile()).thenReturn(Boolean.TRUE);
    when(badHTML.isHidden()).thenReturn(Boolean.FALSE);
    when(badHTML.lastModified()).thenReturn(System.currentTimeMillis());
    when(badHTML.mkdir()).thenReturn(Boolean.FALSE);
    when(badHTML.mkdirs()).thenReturn(Boolean.FALSE);
    when(badHTML.setReadOnly()).thenReturn(Boolean.FALSE);
    when(badHTML.setExecutable(true)).thenReturn(Boolean.FALSE);
    when(badHTML.setExecutable(false)).thenReturn(Boolean.TRUE);
    when(badHTML.setReadOnly()).thenReturn(Boolean.FALSE);

    try {
        BufferedWriter bw = new BufferedWriter(new FileWriter(badHTML));
        /*
          badHTMLText is a string with the contents i want to put into the file, 
          can be just about whatever you want
         */
        bw.append(badHTMLText);
        bw.close();

    } catch (IOException ex) {
        System.err.println(ex);
    }
}

Any ideas or guidance would be very helpful. Somewhere after this i basically try to read from the file using another class. I would try to mock some sort of input stream, but the other class doesn't take an inputstream, since it's the io handling class for the project.

Matej
  • 607
  • 1
  • 5
  • 17
  • What is the objective of the test? WHy not mock the BufferedWriter? – Wand Maker Jul 16 '13 at 16:22
  • BufferedWriter was to write files into the file so that the mocked file could be processed by another class. The other test is for testing and grading the contents of the html file. – Matej Jul 16 '13 at 19:24
  • 1
    Rather than mocking File, try spy(new File(...)). Then you can when() only the behavior you really want to change rather than having to deal with the entire API. – Chris Kessel Jul 17 '13 at 00:40
  • 15
    **DON'T mock or spy a File!** This is not the proper way to do it. It's a test anti-pattern. More generally **Don't mock types you don't own!**. Instead focus a splitting the functionality between the code that access the file and the code that process the content. Jeff's answer is the way to go ! – bric3 Jul 17 '13 at 09:05
  • I would say you absolute can (and should under certain circumstances) mock code you don't own - BUT ONLY the interfaces! Your code should be working against interfaces anyway, not concrete implementations, where possible. java.io.File does not meet the interface requirement though. So BAD candidate for mocking. – demaniak Mar 07 '18 at 05:09

4 Answers4

81

You seem to be after contradictory goals. On the one hand, you're trying to avoid writing data to disk, which isn't a bad goal in tests. On the other, you're trying to test your I/O-handling class, which means you'll be working with system utilities that assume that your File will work with native calls. As such, here's my guidance:

  • Don't try to mock a File. Just don't. Too many native things depend on it.
  • If you can, split your I/O-handling code into the half that opens a File and turns it into a Reader, and the half that parses HTML out of the Reader.
  • At that point, you don't need a mock at all--just construct a StringReader to simulate the data source.
  • While that handles your unit tests pretty well, you may also want to write an integration test that uses a temporary file and ensure that it reads right. (Thanks Brice for adding that tip!)

Don't be afraid to refactor your class to make testing easier, as here:

class YourClass {
  public int method(File file) {
    // do everything here, which is why it requires a mock
  }   
}   

class YourRefactoredClass {
  public int method(File file) {
    return methodForTest(file.getName(), file.isFile(),
        file.isAbsolute(), new FileReader(file));
  }   

  /** For testing only. */
  int methodForTest(
      String name, boolean isFile, boolean isAbsolute, Reader fileContents) {
    // actually do the calculation here
  }   
}   

class YourTest {
  @Test public int methodShouldParseBadHtml() {
    YourRefactoredClass yrc = new YourRefactoredClass();
    assertEquals(42, yrc.methodForTest(
        "bad.html", true, false, new StringReader(badHTMLText));
  }   
}   

At this point the logic in method is so straightforward it's not worth testing, and the logic in methodForTest is so easy to access that you can test it heavily.

Community
  • 1
  • 1
Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
  • 14
    +1 for not mocking a file and splitting the logic into different object with different concerns. **Also in a more general way it is not a good idea at all to mock types you don't own.** So I'm rephrasing what Jeff said, but write an **integration test** that will assert the code that will open and/or write the file and provide either your own types or a `Reader`/`Writer` and write a **unit test** for the code that will process the content, of course you won't have to mock the `Reader`/`Writer` but use or assert content on the existing implementations like the `StringReader`/`StringWriter`. – bric3 Jul 17 '13 at 08:58
  • Yes, that is what I would do if I could edit all the way down to it. I technically _can_ edit it, but can't because it would break present usability for others. – Matej Jul 17 '13 at 13:52
  • Matt--like with `method` above, you can often insert your own test access without actually changing the external API. I understand that it may not be your part of the project, but if your job is to test someone else's hard-to-test class, that's generally worth trying to move things around to make it cleaner and easier. Good luck! :) – Jeff Bowman Jul 17 '13 at 14:58
  • Thanks. I decided to by-pass it altogether and not even try to mock these, and just have to use real files, since trying to mock anything or substitute anything got in the way of the rest of the work. – Matej Jul 24 '13 at 13:40
  • 1
    How about using a virtual file system, such as JIMFS? When i test a service that copies a file form A to B and I let it copy inside the virtual file system, is that still a unit test? is that a good idea at all? – user3629892 Dec 16 '16 at 09:41
  • @user3629892 Clever! That's probably just fine, bearing in mind that NIO was designed for that extra layer of abstraction that java.io.File typically lacks. Just realize it's [not a drop-in replacement](https://www.google.com/amp/www.javaworld.com/article/2078654/core-java/java-se-five-ways-to-maximize-java-nio-and-nio-2.amp.html) and that mocking NIO _classes_ is still a bad idea rendered unnecessary by in-memory behavior. I'd still call it a unit test but I'll leave that for the philosophers. – Jeff Bowman Dec 16 '16 at 15:48
6

One way to mock I/O calls (with Java 7 it would be the NIO final class java.nio.file.Files) is to wrap the needed calls in your own class and mock it:

public class FileHelper {

    public Path createDirectory(String directoryName) throws IOException {
        return Files.createDirectory(Paths.get(directoryName));
    }

    public boolean exists(String name) throws IOException {
        return Files.exists(Paths.get(name), LinkOption.NOFOLLOW_LINKS);
    }

}

The business logic is located in the ImageManager:

FileHelper fileHelperMock = Mockito.mock(new FileHelper());
ImageManager imageManager = new ImageManagerImpl(fileHelperMock);

The test will verify the call to createDirectory() method on your mock:

imageManager.save("directory");
Mockito.verify(fileHelperMock).createDirectory("directory");

I would use this method during test driven development where I do not want to pollute testing with real file management (e.g. delete created directories/files in a finally block in each unit test).

Then I would have acceptance tests covering every use case with real file handling.

Tian Na
  • 846
  • 2
  • 15
  • 30
3

Another aproach is to generate a temporary file in an @Before block and then delete it in a @After method.

Alan Hernandez
  • 309
  • 3
  • 5
1

This is not technically a mock but I have had success using a File reference to /dev/null which can be used in test code without performing any actual I/O (see this answer on Unix SE). Of course, it will always read as an empty file and writes to it will not be persisted, but it can otherwise act as a valid file, open streams, etc.

Patrick Goley
  • 5,397
  • 22
  • 42