如何单元测试文件访问(Java)?

如何单元测试文件访问(Java)?,java,unit-testing,Java,Unit Testing,我知道一个好的单元测试不应该访问文件系统。所以我也知道,您可以使用Mockito和PowerMock来模拟File类 但是下面的代码呢 public ClassLoaderProductDataProvider(ClassLoader classLoader, String tocResourcePath, boolean checkTocModifications) { // ... this.cl = classLoader; tocUrl = cl.getResou

我知道一个好的单元测试不应该访问文件系统。所以我也知道,您可以使用Mockito和PowerMock来模拟File类

但是下面的代码呢

public ClassLoaderProductDataProvider(ClassLoader classLoader, String tocResourcePath, boolean checkTocModifications) {
    // ...
    this.cl = classLoader;
    tocUrl = cl.getResource(tocResourcePath);
    if (tocUrl == null) {
        throw new IllegalArgumentException("Can' find table of contents file " + tocResourcePath);
    }
    this.checkTocModifications = checkTocModifications;
    toc = loadToc();
    // ...
}

private ReadonlyTableOfContents loadToc() {
    InputStream is = null;
    Document doc;
    try {
        is = tocUrl.openStream();
        doc = getDocumentBuilder().parse(is);
    } catch (Exception e) {
        throw new RuntimeException("Error loading table of contents from " + tocUrl.getFile(), e);
    } finally {
        if (is != null) {
            try {
                is.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
    try {
        Element tocElement = doc.getDocumentElement();
        ReadonlyTableOfContents toc = new ReadonlyTableOfContents();
        toc.initFromXml(tocElement);
        return toc;
    } catch (Exception e) {
        throw new RuntimeException("Error creating toc from xml.", e);
    }
}
该类使用位于toc资源的文件内容初始化其toc属性

因此,我想到的第一件测试是创建一个子类,它不在构造函数中调用super,因此所有的文件访问都不会完成。在我自己的构造函数中,然后我为应该从文件中读取的数据插入测试伪数据。然后我可以毫无问题地测试班上的其他同学


但是,原始类的构造函数代码根本没有经过测试。如果出现错误怎么办?

您是从哪里想到“好的”单元测试不应该访问文件系统的?只要测试可以在多种环境下重复,就没有问题。因此,在本例中,这意味着您在测试类路径上创建了一个静态文件,并将该文件的路径传递给ClassLoaderProductDataProvider构造函数。无需使其变得更复杂。

访问文件系统对于单元测试来说是完全可以接受的。事实上,有一整套文件作为测试系统的固定装置是很常见的。它使添加新测试变得很容易,因为您不需要添加新代码,只需要添加数据。

您可以传入一个自定义类加载器,该加载器在调用时提供tocUrl的测试实例。但是,为什么要传入类装入器呢?如果您只使用tocUrl,只需传入它,而不是类加载器和存根。它大大简化了事情

public ClassLoaderProductDataProvider(ClassOfToUrl tocUrl, String tocResourcePath, boolean checkTocModifications) {
// ...
this.tocUrl = tocUrl;

这里的问题是,您的构造函数正在进行工作以及设置状态。为了便于测试,您确实希望将这两项任务分开。你可以明白为什么,你都纠结在一起了

问题是:通常,为了使适当的单元测试能够工作,您需要为类提供接口,而不是具体的类,以允许您灵活地进行不同的测试。看看你的例子,在我看来,你应该将加载
文档的责任提取到其他类中。。。使用名为
DocumentSource
的接口

那么这里的代码就完全不依赖于文件系统了。可能看起来像

public SomethingProductDataProvider(DocumentSource source, String tocDocumentName,
                                    boolean checkTocModifications) {
  this.source = source;
  this.tocDocumentName = tocDocumentName;
  this.checkTocModifications = checkTocModifications;
  this.toc = loadToc();
}

private ReadonlyTableOfContents loadToc() {
  Document doc = source.getDocument(tocDocumentName);
  if (doc == null) {
    throw new IllegalArgumentException("Can' find table of contents file " + 
        tocResourcePath);
  }

  try {
    Element tocElement = doc.getDocumentElement();
    ReadonlyTableOfContents toc = new ReadonlyTableOfContents();
    toc.initFromXml(tocElement);
    return toc;
  } catch (Exception e) {
    throw new RuntimeException("Error creating toc from xml.", e);
  }
}
或者,您可以让类直接在其构造函数中获取
文档
,甚至是
输入流
。当然,在某些时候,您必须使用
类加载器
从资源中加载
InputStream
的实际代码。。。但是你可以把代码推到一个简单的程序中,而这个程序只能做到这一点。很明显,您对该类所做的任何测试都必须使用实际的文件。。。但其他类的测试不受影响

作为旁注,如果类在其构造函数中工作(例如,在本例中加载目录),那么对于类的可测试性来说,这是一个不好的迹象。这里可能有一种更好的设计类的方法,它消除了对类的需求,并且更易于测试,但是很难说这个设计到底给出了什么


您还可以使用多种其他选项,包括使用像Guava的界面之类的东西,并结合已经测试过的工厂方法,例如获取用于生产的
InputSupplier
实例。然而,关键的一点是始终让类依赖于接口,以便在测试中轻松使用替代实现。

单元测试只是测试工具包中的一个工具。但与所有工具一样,它也有一个预期目的和有限的适用范围。Roy Osherove的单元测试艺术可以解释,当涉及外部依赖时,单元测试是不合适的。这是因为在这个问题的其他地方已经说明了一些原因:保持测试运行时的速度,使测试可以在开发人员环境中重复,消除错误的测试失败,等等


从文件系统读取,即使这是一段代码的一个完整任务,也是一种外部依赖。所以在我看来,你们试图强迫这种情况是单元可测试的,而实际上不是。您应该尽可能使用模拟库和良好的解耦设计进行单元测试,并使用其他测试工具(如手动或自动集成测试)来测试外部依赖项。

您不能避免调用超类构造函数。如果不显式调用它,编译器将尝试生成对零参数默认构造函数的调用。如果它不存在,那是一个编译时错误。可以在原始类中创建一个零参数默认构造函数,它什么也不做。或者你可以使用PowerMocks WhiteBox.newInstance方法。如果你搜索“良好的单元测试”或“单元测试最佳实践”,你几乎可以在任何地方阅读它。此外,它使单元测试速度大大降低,这对TDD不利。您可以随时遇到IOExceptions(例如,当测试在另一台服务器上运行而没有必要的权限时)。如果您依赖某个文件不存在以便可以创建它,但前一次测试运行没有删除该文件,那么您可能会遇到问题……文件系统是一个外部依赖项,根据单元测试的许多定义,它使依赖它的测试成为某种类型的集成测试。避免在开发过程中一直运行的单元测试中访问文件系统会使它们运行得更快,并消除与被测试类无关的可能的测试失败源。当然,如果你只是指单元测试中的“自动测试”,那情况就不同了。@user519499:如果测试文件是源代码树的一部分,那么就不应该有任何权限问题(如果有,那么你就有更大的问题。)如果你对未获取的文件有问题