如何测试这个文件写入,3lines 功能?

How to test this file-writing, 3lines function?

这是我在某些服务中的方法class。它是 public 所以它应该被测试。我根本不知道我应该测试什么。我会模拟 Writer 和 spyOn 函数调用,但使用此实现是不可能的(不是吗?)

我正在使用 MockitoJUnit

现在,我只能创建函数来抛出并断言该异常

有什么帮助吗?

@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()
        );
    }
}

为了测试您的方法是否可以通过发送正确的命令与编写器正确交互,您的程序必须公开某种 "seam" 以便您的测试可以配置模拟 FileWriter。我不熟悉 mockito 但一种方法是将 FileWriter 实例化封装在方法后面,然后您的测试可以将该方法重写为 return 模拟 FileWriter.

假设File是一个接口:

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

这可以让您覆盖上述方法进行测试和 return 伪造 Writer

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

然后您的测试可以进行练习 initIndexFile 并对操作进行断言。使用模拟文件编写器应该很容易抛出 IOException 以便您可以练习错误处理逻辑。

您可以简单地在测试中为您的方法提供一个临时文件,然后简单地检查它是否包含预期的 [],一旦超过就删除该文件。

类似于:

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()))); 
    }
}

注意 1: 我依赖 new String(byte[]) 这意味着我依赖默认字符编码,就像您在当前代码中所做的那样,但这不是一个好习惯, 我们应该明确设置字符编码以避免平台依赖。

注意2:假设你使用java7或更高版本,你应该考虑使用try-with-resources 声明正确关闭你的作者,你的代码将是:

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()
        );
    }
}

模拟依赖项是可能的,也是自然的,但是模拟在方法主体中声明的对象是不自然且棘手的。

我设想了 3 种解决方案:

1) 为什么不简单地断言文件是用预期的字符写入的,而不是模拟?

它避免了技巧,但如果您经常执行此任务并且想要对其进行单元测试,它可能会多余且缓慢。

2) 使局部变量成为一个实例字段来模拟它。这似乎真的不是一个干净的解决方案。如果您在执行此类处理的同一个 class 中有多个方法,则可能会重复使用同一个编写器或具有多个编写器字段。在这两种情况下,您都可能会产生副作用。

3) 如果你执行很多写操作并且你想真正隔离对编写器的调用,你有一个解决方案:重新设计你的代码以获得可测试的 class.

您可以提取依赖项来执行编写器处理。 class 可以提供一种带有执行指令所需参数的方法。我们可以称它为: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());
       }
     }
 }

这个 class 是可测试的,因为 writer 依赖是一个参数。

你这样调用新服务:

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,"[]");
  }
}

现在 initIndexFile() 也可以通过模拟 WriteService 进行测试。 您可以检查 writeAndClose() 是在 writeService 上使用 good 参数调用的。

就我个人而言,我会使用第一种解决方案或第三种解决方案。

如果您认为添加特殊内容是业务逻辑,因此是您class的责任,然后创建 FileWriter 不是(根据 单一责任模式

因此您应该使用在测试 下注入您的Class 的FileWriterFactory。然后你可以模拟 FileWriterFactory 到 return Writer 接口的模拟实现,然后你可以检查它是否得到了预期的字符串。

你的 CuT 会变成这样:

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()
        );
    }
}

你对此的测试:

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();
  }
}

此外,您可以轻松检查您的 CuT 是否拦截了 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

这是一个很好的观点。

问题是:class 中是否有任何方法直接与 File 对象交互并且之后需要创建 FileWriter?

如果答案是 "no"(这很可能)遵循 KISS 原则,您应该直接注入一个 Writer 对象而不是工厂,并且您的方法没有 File 参数。

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()
        );
    }
}

修改后的测试:

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);
  }
}