Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I mock Java Path API with Mockito?

Java Path API is a better replacement of Java File API but massive usage of static methods makes it difficult to mock with Mockito. From my own class, I inject a FileSystem instance which I replace with a mock during unit tests.

However, I need to mock a lot of methods (and also creates a lot of mocks) to achieve this. And this happens repeatedly so many times across my test classes. So I start thinking about setup a simple API to register Path-s and declare associated behaviour.

For example, I need to check error handling on stream opening. The main class:

class MyClass {
    private FileSystem fileSystem;

    public MyClass(FileSystem fileSystem) {
        this.fileSystem = fileSystem;
    }

    public void operation() {
        String filename = /* such way to retrieve filename, ie database access */
        try (InputStream in = Files.newInputStream(fileSystem.getPath(filename))) {
            /* file content handling */
        } catch (IOException e) {
            /* business error management */
        }
    }
}

The test class:

 class MyClassTest {

     @Test
     public void operation_encounterIOException() {
         //Arrange
         MyClass instance = new MyClass(fileSystem);

         FileSystem fileSystem = mock(FileSystem.class);
         FileSystemProvider fileSystemProvider = mock(FileSystemProvider.class);
         Path path = mock(Path.class);
         doReturn(path).when(fileSystem).getPath("/dir/file.txt");
         doReturn(fileSystemProvider).when(path).provider();
         doThrow(new IOException("fileOperation_checkError")).when(fileSystemProvider).newInputStream(path, (OpenOption)anyVararg());

         //Act
         instance.operation();

         //Assert
         /* ... */
     }

     @Test
     public void operation_normalBehaviour() {
         //Arrange
         MyClass instance = new MyClass(fileSystem);

         FileSystem fileSystem = mock(FileSystem.class);
         FileSystemProvider fileSystemProvider = mock(FileSystemProvider.class);
         Path path = mock(Path.class);
         doReturn(path).when(fileSystem).getPath("/dir/file.txt");
         doReturn(fileSystemProvider).when(path).provider();
         ByteArrayInputStream in = new ByteArrayInputStream(/* arranged content */);
         doReturn(in).when(fileSystemProvider).newInputStream(path, (OpenOption)anyVararg());

         //Act
         instance.operation();

         //Assert
         /* ... */
     }
 }

I have many classes/tests of this kind and mock setup can be more tricky as static methods may call 3-6 non-static methods over the Path API. I have refactored test to avoid most redundant code but my simple API tends to be very limited as my Path API usage grown. So again it's time to refactor.

However, the logic I'm thinking about seems ugly and requires much code for a basic usage. The way I would like to ease API mocking (whatever is Java Path API or not) is based on the following principles:

  1. Creates abstract classes that implements interface or extends class to mock.
  2. Implements methods that I don't want to mock.
  3. When invoking a "partial mock" I want to execute (in preference order) : explicitly mocked methods, implemented methods, default answer.

In order to achieve the third step, I think about creating an Answer which lookup for implemented method and fallback to a default answer. Then an instance of this Answer is passed at mock creation.

Are there existing ways to achieve this directly from Mockito or other ways to handle the problem ?

like image 701
LoganMzz Avatar asked Aug 28 '15 13:08

LoganMzz


1 Answers

Your problem is that you are violating the Single Responsibility Principle.

You have two concerns:

  1. Find and locate a file, get an InputStream
  2. Process the file.
    • Actually, this should most likely be broken into sub concerns also, but that's outside the scope of this question.

You are attempting to do both of those jobs in one method, which is forcing you to do a ton of extra work. Instead, break the work into two different classes. For example, if your code were instead constructed like this:

class MyClass {
  private FileSystem fileSystem;
  private final StreamProcessor processor;

  public MyClass(FileSystem fileSystem, StreamProcessor processor) {
    this.fileSystem = fileSystem;
    this.processor = processor;
  }

  public void operation() {
    String filename = /* such way to retrieve filename, ie database access */
    try (InputStream in = Files.newInputStream(fileSystem.getPath(filename))) {
        processor.process(in);
    } catch (IOException e) {
        /* business error management */
    }
  }
}
class StreamProcessor {
  public StreamProcessor() {
    // maybe set dependencies, depending on the need of your app
  }

  public void process(InputStream in) throws IOException {
    /* file content handling */
  }
}

Now we've broken the responsibilities into two places. The class that does all the business logic work that you want to test, from an InputStream, just needs an input stream. In fact, I wouldn't even mock that, because it's just data. You can load the InputStream any way you want, for example using a ByteArrayInputStream as you mention in your question. There doesn't need to be any code for Java Path API in your StreamProcessor test.

Additionally, if you are accessing files in a common way, you only need to have one test to make sure that behavior works. You can also make StreamProcessor be an interface, and then, in the different parts of your code base, do the different jobs for different types of files, while passing in different StreamProcessors into the file API.


In the comments you said:

Sounds good but I have to live with tons of legacy code. I'm starting to introduce unit test and don't want to refactor too much "application" code.

The best way to do it is what I said above. However, if you want to do the smallest amount of changes to add tests, here is what you should do:

Old code:

public void operation() {
  String filename = /* such way to retrieve filename, ie database access */
  try (InputStream in = Files.newInputStream(fileSystem.getPath(filename))) {
    /* file content handling */
  } catch (IOException e) {
    /* business error management */
  }
}

New code:

public void operation() {
  String filename = /* such way to retrieve filename, ie database access */
  try (InputStream in = Files.newInputStream(fileSystem.getPath(filename))) {
    new StreamProcessor().process(in);
  } catch (IOException e) {
    /* business error management */
  }
}
public class StreamProcessor {
  public void process(InputStream in) throws IOException {
    /* file content handling */
    /* just cut-paste the other code */
  }
}

This is the least invasive way to do what I describe above. The original way I describe is better, but obviously it's a more involved refactor. This way should involve almost no other code changes, but will allow you to write your tests.

like image 88
durron597 Avatar answered Oct 13 '22 12:10

durron597