如何使用Mockito模拟Java Path API?

Log*_*Mzz 5 java unit-testing path mocking mockito

Java Path API是Java File API的更好替代品,但静态方法的大量使用使得很难使用Mockito进行模拟.从我自己的类中,我注入一个FileSystem实例,在单元测试期间我用模拟替换它.

但是,我需要模拟很多方法(并且还创建了很多模拟)来实现这一点.在我的测试课程中,这种情况反复发生了很多次.所以我开始考虑设置一个简单的API来注册Path-s并声明相关的行为.

例如,我需要检查流开放时的错误处理.主要课程:

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 */
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

测试类:

 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
         /* ... */
     }
 }
Run Code Online (Sandbox Code Playgroud)

我有很多这种类/测试,并且模拟设置可能更棘手,因为静态方法可能会通过Path API调用3-6个非静态方法.我已经重构测试以避免大多数冗余代码,但随着我的Path API使用量的增长,我的简单API往往非常有限.所以再次重构是时候了.

但是,我正在考虑的逻辑似乎很难看,并且需要很多代码才能用于基本用法.我希望简化API模拟(无论是否是Java Path API)的方式基于以下原则:

  1. 创建实现接口的抽象类或将类扩展为mock.
  2. 实现我不想模拟的方法.
  3. 当调用"部分模拟"时,我想执行(按优先顺序):显式模拟方法,实现方法,默认答案.

为了实现第三步,我考虑创建一个Answer查找已实现的方法并回退到默认答案.然后Answer在模拟创建时传递一个这样的实例.

是否有现成的方法直接从Mockito或其他方式来解决这个问题?

dur*_*597 5

您的问题是您违反了单一责任原则.

你有两个问题:

  1. 找到并找到一个文件,得到一个 InputStream
  2. 处理文件.
    • 实际上,这很可能也会被分解为子问题,但这超出了这个问题的范围.

你试图用一种方法做这两种工作,这迫使你做大量的额外工作.相反,将工作分成两个不同的类.例如,如果你的代码是这样构造的:

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 */
    }
  }
}
Run Code Online (Sandbox Code Playgroud)
class StreamProcessor {
  public StreamProcessor() {
    // maybe set dependencies, depending on the need of your app
  }

  public void process(InputStream in) throws IOException {
    /* file content handling */
  }
}
Run Code Online (Sandbox Code Playgroud)

现在我们把责任分为两个地方.执行所有要测试的业务逻辑工作的类InputStream,只需要一个输入流.事实上,我甚至不会嘲笑它,因为它只是数据.您可以以InputStream任何方式加载,例如使用ByteArrayInputStream您在问题中提到的方式.您的StreamProcessor测试中不需要任何Java Path API代码.

此外,如果您以通用方式访问文件,则只需要进行一次测试即可确保该行为有效.您还可以创建StreamProcessor一个接口,然后在代码库的不同部分为不同类型的文件执行不同的作业,同时将不同的StreamProcessors 传递到文件API中.


在评论中你说:

听起来不错,但我必须忍受大量遗留代码.我开始介绍单元测试,不想重构太多的"应用程序"代码.

最好的方法就是我上面所说的.但是,如果您想进行少量的更改以添加测试,那么您应该执行以下操作:

旧代码:

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 */
  }
}
Run Code Online (Sandbox Code Playgroud)

新代码:

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 */
  }
}
Run Code Online (Sandbox Code Playgroud)
public class StreamProcessor {
  public void process(InputStream in) throws IOException {
    /* file content handling */
    /* just cut-paste the other code */
  }
}
Run Code Online (Sandbox Code Playgroud)

这是我进行上述描述的侵入性最小的方法.我描述的原始方式更好,但显然它是一个更复杂的重构.这种方式几乎不涉及其他代码更改,但允许您编写测试.