3

This is my method in some service class. It's public so it should be tested. I simply do not know WHAT should I test. I'd mock Writer and spyOn function call, but with this implementation it's impossible (isn't it?)

I'm using Mockito and JUnit

For now, I can only make function to throw and assert that exception

Any help?

@Override
public void initIndexFile(File emptyIndexFile) {
    try {
        Writer writer = new FileWriter(emptyIndexFile);
        writer.write("[]");
        writer.close();
    } catch (IOException e) {
        throw new IndexFileInitializationException(
            "Error initialization index file " + emptyIndexFile.getPath()
        );
    }
}
Nicolas Filotto
  • 43,537
  • 11
  • 94
  • 122
Konrad Dziurdź
  • 717
  • 3
  • 8
  • 15
  • 3
    Why you think it should be tested. there is no buissines logic in the method – Jens Jan 23 '17 at 14:48
  • 1
    Seems to me that having "[]" represent an empty index is "business logic". In any event it's something that somebody could change which could affect callers of this public API. – John Hascall Jan 23 '17 at 15:01
  • Simple, the test should verify the method under test does what it's supposed to do, namely that it creates a text file with a given path/name and with `"[]"` as its contents. There is nothing to be mocked in this test, and it can easily be written by simply reading the expected output file. – Rogério Jan 23 '17 at 15:09
  • @Rogério I think that there is a need to support a method for providing a stub object so that the `IOException` branch can be exercised. – dm03514 Jan 23 '17 at 15:52

4 Answers4

7

If you feel that adding the the special content is the business logic and therefore the responsibility of your class, then creating the FileWriter is not (according to the single responsibility pattern.

So you should use a FileWriterFactory that is injected into your Class under Test. Then you can mock that FileWriterFactory to return a mock implementation of the Writer interface on which in turn you can check that it got the expected String.

Your CuT would change to this:

private final WriterFactory writerFactory;

public ClassUnderTest(@Inject WriterFactory writerFactory){
   this.writerFactory = writerFactory;
}

@Override
public void initIndexFile(File emptyIndexFile) {
    try {
        Writer writer = writerFactory.create(emptyIndexFile);
        writer.write("[]");
        writer.close();
    } catch (IOException e) {
        throw new IndexFileInitializationException(
            "Error initialization index file " + emptyIndexFile.getPath()
        );
    }
}

and your test to this:

class Test{

  @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); 

  @Mock
  private FileWriterFactory fileWriterFactory;
  private Writer fileWriter = spy(new StringWriter());
  File anyValidFile = new File(".");
  @Test
  public void initIndexFile_validFile_addsEmptyraces(){
     //arrange
     doReturn(fileWriter).when(fileWriterFactory).create(any(File.class));

     // act
     new ClassUnderTest(fileWriterFactory).initIndexFile(anyValidFile);

     //assert
     verify(fileWriterFactory)create(anyValidFile);
     assertEquals("text written to File", "[]", fileWriter.toString());
     verify(fileWriter).close();
  }
}

in addition you could easily check that your CuT intercepts the IOException:

  @Rule
  public ExpectedException exception = ExpectedException.none();

  @Test
  public void initIndexFile_missingFile_IndexFileInitializationException(){
     //arrange
     doReturnThrow(new IOException("UnitTest")).when(fileWriterFactory).create(any(File.class));

     //assert
     exception.expect(IndexFileInitializationException.class);
     exception.expectMessage("Error initialization index file "+anyValidFile.getPath());

     // act
     new ClassUnderTest(fileWriterFactory).initIndexFile(anyValidFile);
  }

Nice! a factory just to test 3 lines of code! – Nicolas Filotto

This is a good point.

The question is: will there be any method within that class ever interacting with the File object directly and needs to create the FileWriter afterwards?

If the answer is "no" (as it is most likely) following the KISS principle you should inject a Writer object directly instead of the factory and have your methods without the File parameter.

private final Writer writer;

public ClassUnderTest(@Inject Writer writer){
   this.writer = writer;
}

@Override
public void initIndexFile() {
    try {
        writer.write("[]");
        writer.close();
    } catch (IOException e) {
        throw new IndexFileInitializationException(
            "Error initialization index file " + emptyIndexFile.getPath()
        );
    }
}

modified test:

class Test{       
  @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); 
  @Rule public ExpectedException exception = ExpectedException.none();

  @Mock
  private FileWriterFactory fileWriterFactory;
  @Mock
  private Writer failingFileWriter;
  private Writer validFileWriter = spy(new StringWriter());
  File anyValidFile = new File(".");
  @Test
  public void initIndexFile_validFile_addsEmptyraces(){
     //arrange         
     // act
     new ClassUnderTest(validFileWriter).initIndexFile();

     //assert
     verify(fileWriterFactory)create(anyValidFile);
     assertEquals("text written to File", "[]", fileWriter.toString());
     verify(fileWriter).close();
  }

  @Test
  public void initIndexFile_missingFile_IndexFileInitializationException(){
     //arrange
     doReturnThrow(new IOException("UnitTest")).when(failingFileWriter).write(anyString());

     //assert
     exception.expect(IndexFileInitializationException.class);
     exception.expectMessage("Error initialization index file "+anyValidFile.getPath());

     // act
     new ClassUnderTest(fileWriterFactory).initIndexFile(anyValidFile);
  }
}
Phe0nix
  • 157
  • 4
  • 15
Timothy Truckle
  • 15,071
  • 2
  • 27
  • 51
  • Wonderful! I've heard tons of vague mutterings about mocking and injecting, but this is the first time I've seen anyone provide a clear, concrete example. (Granted, I've not looked terribly hard...) Really helps me! –  Jan 23 '17 at 17:04
  • *"Nice! a factory just to test 3 lines of code!"* you miss the point that these are most likely only the *first* three lines of code in that class. And yes: this approach works well in my real life projects. – Timothy Truckle Jan 23 '17 at 17:34
0

To test that your method can interact with a writer correctly, by sending the correct commands, your pogram has to expose some sort of "seam" so that your test can configure a mock FileWriter. I'm not familiar with mockito but one way would be to encapsulate the FileWriter instantiation behind a method then your test could override that method to return a mock FileWriter.

Assuming that File is an interface:

public Writer getFileWriter(File emptyIndexFile) {
   return new FileWriter(emptyIndexFile);
}

This could allow you to override the above method for a test and return a fake Writer

@Override
public Writer getFileWriter(File emptyIndexFile) {
   return mockFileWriterInstance;
}

Then your test could make exercise initIndexFile and make assertions on the operations. Using a mock file writer shoudl be trivial to throw IOException so that you can exercise error handling logic.

dm03514
  • 54,664
  • 18
  • 108
  • 145
  • The fact the method under test uses a `FileWriter` is a mere implementation detail (it could just as well use some other file API, such as `RandomAccessFile`). There is nothing to be mocked here. Instead, the test should simply check the expected file was correctly created. – Rogério Jan 23 '17 at 15:15
0

You could simply provide a temporary file to your method in your test and simply check that it contains [] as expected and once over delete the file.

Something like:

public class FileWritingTest {

    // File to provide to the method initIndexFile
    private File file;

    /* This is executed before the test */
    @Before
    public void init() throws IOException {
        // Create a temporary file
        this.file = File.createTempFile("FileWritingTest", "tmp");
        // Indicates that it should be removed on exit
        file.deleteOnExit();
    }

    /* This is executed after the test */
    @After
    public void clean() throws IOException {
        // Delete the file once test over
        file.delete();
    }

    @Test
    public void testInitIndexFile() throws IOException {
        FileWriting fw = new FileWriting();
        // Call the method
        fw.initIndexFile(this.file);
        // Check that the content is [] as expected
        Assert.assertEquals("[]", new String(Files.readAllBytes(file.toPath()))); 
    }
}

NB 1: I rely on new String(byte[]) which means that I rely on the default character encoding like you do in your current code but it is not a good practice, we should set a character encoding explicitly to avoid platform dependent.

NB 2: Assuming that you use java 7 or higher, you should consider using the try-with-resources statement to properly close your writer, your code would then be:

public void initIndexFile(File emptyIndexFile) {
    try (Writer writer = new FileWriter(emptyIndexFile)) {
        writer.write("[]");
    } catch (IOException e) {
        throw new IndexFileInitializationException(
            "Error initialization index file " + emptyIndexFile.getPath()
        );
    }
}
Nicolas Filotto
  • 43,537
  • 11
  • 94
  • 122
  • The method being tested is the one that creates the file, so this answer isn't making any sense... – Rogério Jan 23 '17 at 15:13
  • @Rogério where do you see that it creates a file? it writes into a file – Nicolas Filotto Jan 23 '17 at 15:15
  • Although this is a practical solution it is not an option for a *unit test*. A unitTest should never use something outside the JVM, especially not databases or files. – Timothy Truckle Jan 23 '17 at 16:34
  • @NicolasFilotto *"where have you see in the OP's question that he expects explicitly a unit test?"* I implied that my the OPs mentioning of *mocking* which only makes sense with unit tests IMHO. – Timothy Truckle Jan 23 '17 at 17:37
  • *A unitTest should never use something outside the JVM, especially not databases or files* from where do you get that? provide credible resources to prove what you say otherwise it is purely and simply primary opinion based. – Nicolas Filotto Jan 23 '17 at 19:58
  • @NicolasFilotto [A Set of Unit Testing Rules by Michael Feathers](http://www.artima.com/weblogs/viewpost.jsp?thread=126923) *"A test is not a unit test if: [...] It touches the file system"* – Timothy Truckle Jan 28 '17 at 09:41
  • @Rogério *In computer programming, unit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use... Substitutes such as method stubs, mock objects, fakes, and test harnesses* **can** (not must) *be used to assist testing a module in isolation.* source https://en.wikipedia.org/wiki/Unit_testing – Nicolas Filotto Jan 28 '17 at 10:00
0

Mocking a dependency is possible and natural, but mocking an object declared in the body of the method is not natural and tricky.

I imagine 3 solutions:

1) Why, instead of mocking, could you not simply assert that the file is written with the expected character?

It avoids tricks, but it may be redundant and slow if you perform this task very often and you want to unit test them.

2) Making the local variable an instance field to mock it. This seems really a not at all clean solution. If you have multiple methods in the same class that does this kind of processing, you risk to reuse the same writer or to have multiple writer fields. In both cases, you could have side effects.

3) If you perform many write operations and you want to really isolate the call to the writer, you have a solution: redesign your code to have a testable class.

You could extract a dependency to perform the writer processings. The class could provide a method with required parameters to perform instructions. We could call it : WriteService.

 public class WriteService {
    ...
    public void writeAndClose(Writer writer, String message){
      try {   
        writer.write(message);
        writer.close();
       } 
        catch (IOException e) {
        throw new IndexFileInitializationException("Error initialization index  file " + emptyIndexFile.getPath());
       }
     }
 }

This class is testable because the writer dependency is a parameter.

And you call the new service like that :

public class YourAppClass{

  private WriteService writeService;

  public YourAppClass(WriteService writeService){
    this.writeService=writeService;
  }

  @Override
  public void initIndexFile(File emptyIndexFile) {
        Writer writer = new FileWriter(emptyIndexFile);
        writeService.writeAndClose(writer,"[]");
  }
}

Now initIndexFile() is also testable by mocking WriteService. You could check tat writeAndClose() is called on writeService with the good parameter.

Personally, I would use the first solution or the third solution.

davidxxx
  • 125,838
  • 23
  • 214
  • 215