如何使用Mockito模拟Java Path API?

6

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

测试类:

该测试类:

 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个非静态方法。我已经重构了测试以避免大部分冗余代码,但是我的简单API往往非常有限,因为我的Path API使用量增加了。所以又到了重构的时候。
然而,我考虑的逻辑看起来很丑陋,需要很多基本用法的代码。我想要简化API模拟(无论是Java Path API还是其他),这是基于以下原则:
1. 创建实现接口或扩展类进行模拟的抽象类。 2. 实现我不想模拟的方法。 3. 在调用“部分模拟”时,我希望按照以下顺序执行:显式模拟的方法、已实现的方法、默认答案。
为了实现第三步,我考虑创建一个查找实现方法并回退到默认答案的Answer。然后,在模拟创建时传递此Answer的实例。
是否存在直接从Mockito或其他方式处理问题的方法?
1个回答

7
你的问题是违反了单一职责原则
你有两个问题:
  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 */
    }
  }
}

class StreamProcessor {
  public StreamProcessor() {
    // maybe set dependencies, depending on the need of your app
  }

  public void process(InputStream in) throws IOException {
    /* file content handling */
  }
}

现在,我们将职责分为两个部分。执行所有业务逻辑工作的类(您想要测试的)只需要一个输入流,实际上,我甚至不会模拟它,因为它只是数据,您可以以任何方式加载输入流,例如使用您在问题中提到的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 */
  }
}

新代码:
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 */
  }
}

这是最不侵入性的方法来实现我上面所描述的内容。我原先描述的方法 更好,但显然需要进行更多涉及重构的操作。这种方法几乎不需要进行其他代码更改,但可以让你编写测试。

听起来不错,但我必须与大量的遗留代码共存。我开始引入单元测试,不想过多重构“应用”代码。更多的operation方法只是一个模板,我还需要在文件名检索和错误处理方面进行抽象,以及在文件处理方面进行抽象。最后,“MyClass”变成了一个空壳...目前我只有两个“简单”的类:一个驱动调用第二个(“MyClass”)的“批处理”。第一个继承自全局对象,为我们的批处理基础架构定义了公共操作和流程。我不是SRP极端主义者的粉丝 :( - LoganMzz
1
@mlogan,我向你保证,在这里进行最基本的重构比与PowerMockito Mock静态方法斗争要容易得多。让我编辑一下以进一步解释。 - durron597

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接