如何使用Mockito模拟Java路径API?

如何使用Mockito模拟Java路径API?,java,unit-testing,path,mocking,mockito,Java,Unit Testing,Path,Mocking,Mockito,Java路径API是Java文件API的更好替代品,但是大量使用静态方法使得使用Mockito进行模拟变得困难。 从我自己的类中,我注入了一个文件系统实例,在单元测试期间用一个mock替换它 然而,我需要模拟很多方法(也创建了很多模拟)来实现这一点。这在我的测试类中反复发生了很多次。所以我开始考虑设置一个简单的API来注册Path-s并声明相关的行为 例如,我需要检查流打开时的错误处理。 主要类别: class MyClass { private FileSystem fileSyste

Java路径API是Java文件API的更好替代品,但是大量使用静态方法使得使用Mockito进行模拟变得困难。 从我自己的类中,我注入了一个
文件系统
实例,在单元测试期间用一个mock替换它

然而,我需要模拟很多方法(也创建了很多模拟)来实现这一点。这在我的测试类中反复发生了很多次。所以我开始考虑设置一个简单的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 */
        }
    }
}
测试类:

 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
         /* ... */
     }
 }
我有很多此类的类/测试,模拟设置可能会更复杂,因为静态方法可能会通过Path API调用3-6个非静态方法。我已经重构了测试以避免大多数冗余代码,但随着Path API使用量的增加,我的简单API往往非常有限。因此,再次是重构的时候了

然而,我所考虑的逻辑看起来很难看,需要很多代码来实现基本用法。我希望简化API模拟的方式(无论是否为Java路径API)基于以下原则:

  • 创建实现接口或将类扩展到mock的抽象类
  • 实现我不想模仿的方法
  • 当调用我想要执行的“部分模拟”时(按优先顺序):显式模拟的方法、实现的方法、默认答案
  • 为了实现第三步,我考虑创建一个
    Answer
    ,查找实现的方法并返回默认答案。然后在模拟创建时传递此
    应答的一个实例


    有没有直接从Mockito或其他处理问题的方法来实现这一点的现有方法?

    您的问题是您违反了标准

    你有两个顾虑:

  • 查找并定位文件,获取
    InputStream
  • 处理该文件。
    • 事实上,这很可能也被分解成子关注点,但这超出了这个问题的范围
  • 您正试图用一种方法完成这两项工作,这会迫使您做大量额外的工作。相反,将作品分成两个不同的类别。例如,如果您的代码是这样构造的:

    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 */
        }
      }
    }
    
    现在我们把责任分为两部分。从
    InputStream
    执行您想要测试的所有业务逻辑工作的类只需要一个输入流。事实上,我甚至不会嘲笑它,因为它只是数据。您可以按任何方式加载
    InputStream
    ,例如使用您在问题中提到的
    ByteArrayInputStream
    您的
    StreamProcessor
    测试中不需要任何Java路径API代码

    此外,如果您以常用方式访问文件,则只需进行一次测试即可确保该行为正常工作。您还可以将
    StreamProcessor
    设置为一个接口,然后在代码库的不同部分,为不同类型的文件执行不同的作业,同时将不同的
    StreamProcessor
    传递到文件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 */
      }
    }
    
    新代码:

    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 */
      }
    }
    

    这是做我上面描述的事情的最不具侵入性的方法。我最初描述的方式更好,但显然这是一种更复杂的重构。这种方式几乎不涉及其他代码更改,但允许您编写测试。

    听起来不错,但我不得不接受大量遗留代码。我开始介绍单元测试,不想重构太多的“应用程序”代码。更多的
    操作
    方法只是一个模板,除了对文件处理进行抽象之外,我还需要对文件名检索和错误处理进行抽象。最后,“MyClass”变成了一个空壳。。。目前我只有两个“简单”类:一个驱动调用第二个(“MyClass”)的“批处理”。第一个继承自定义批处理基础结构的通用操作和流的全局。我不是SRP极端主义的粉丝:(@mlogan我绝对向你保证,对你来说,在这里进行最基本的重构比使用PowerMockito模拟静态方法更容易。让我编辑一下以进一步解释。
    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 */
      }
    }