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:
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 ?
Your problem is that you are violating the Single Responsibility Principle.
You have two concerns:
InputStream
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 StreamProcessor
s 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With