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

7
我知道一个好的单元测试不应该访问文件系统。因此,我也知道你可以使用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);
    }
}

这个类会通过位于tocResource的文件的内容来初始化它的toc属性。
因此,对于测试,我首先想到的是创建一个子类,在构造函数中不调用super,这样所有的文件访问都不会执行。然后在我的构造函数中,我插入了应该从文件中读取的测试虚拟数据。这样我就可以毫无问题地测试类的其余部分。
然而,原始类的构造函数代码根本没有被测试过。如果出现错误怎么办?

1
你不能避免调用父类的构造函数。如果你没有明确地调用它,编译器会尝试生成对零参数默认构造函数的调用。如果该构造函数不存在,这将导致编译时错误。 - Rytmis
可以在原始类中创建一个零参数默认构造函数,它仅仅不做任何事情。或者可以使用PowerMocks WhiteBox.newInstance方法。 - user519499
5个回答

9
这是件事情:通常,为了使适当的单元测试工作,您需要为您的类提供接口而不是具体类,以允许您在测试时进行不同的操作。看看您的示例,我认为您应该将加载文档的责任提取到其他类中...使用名为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);
  }
}

或者,你可以在类的构造函数中直接使用Document甚至是InputStream。当然,在某个时候,你必须有实际加载InputStream资源的代码,使用ClassLoader...但你可以将该代码推入仅执行此操作的简单对象中。然后,很明显,任何对于该类的测试都必须使用实际文件...但其他类的测试不受影响。

另外需要注意的是,如果一个类在其构造函数中执行工作(例如在这种情况下加载目录表),那么这是一个不好的可测试性迹象。在涉及到的类中可能有更好的设计方式来消除这种需求并且更容易进行测试,但仅凭这些信息很难确切地说明该设计方式是什么。

除此之外,您还可以选择其他选项,包括使用类似Guava的InputSupplier接口,结合已经经过测试的工厂方法,比如Resources.newInputStreamSupplier(URL),以获取InputSupplier实例用于生产。然而,关键是始终使您的类依赖于接口,以便在测试中轻松使用替代实现。


4

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


+10!我不明白为什么要在术语上斤斤计较。我经常使用这种技术。如果有人想强迫我称其为“集成测试”,那没问题,只要它不被归类为与数据库或其他99.999%的时间无法访问的东西相同即可,而文件系统则不是这样的。 - Droj

3

您从何得出“好的”单元测试不应访问文件系统的想法?只要测试在多个环境中可重现,就没有问题。因此,在这种情况下,您需要在测试类路径上创建一个静态文件,并将该文件的路径传递给ClassLoaderProductDataProvider构造函数。不需要使它更加复杂。


4
如果您在谷歌上搜索“好的单元测试”或“单元测试最佳实践”,几乎可以在任何地方阅读到相关内容。此外,使用文件操作会使单元测试变得相当缓慢,这对于TDD来说不利。可能会在任何时候遇到IOException(例如,当测试在没有必要权限的另一台服务器上运行时)。如果依赖于某个文件不存在,因此您可以创建它,但前一个测试运行未删除该文件,则可能会遇到问题... - user519499
4
文件系统是一个外部依赖项,按照许多单元测试的定义,依赖于它的测试就成为某种集成测试。在开发过程中经常运行的单元测试中避免文件系统访问可以使它们运行更快,并消除与被测试类实际无关的测试失败可能性。当然,如果你所说的"单元测试"只是指"自动化测试",那就是另一回事了。 - ColinD
2
如果测试文件是源代码树的一部分,那么就不应该有任何权限问题(如果有,那么你可能有更大的问题)。如果您在未清理的文件上遇到问题,请在测试之前清理它们。至于“单元”与“集成”的争论,在我看来完全没有意义。作为开发人员,我的工作是确保代码能够正常工作 - 我没有时间纠缠于此。 - Mike Baranczak
2
无论如何,这都不是重点。读取文件系统是这段代码的功能:没有访问文件系统就没有好的测试方法。 - Mike Baranczak
3
这段代码不应该“需要”读取文件系统......它有其他的职责,应该能够独立于文件系统进行测试。可以将文件系统访问推到其他地方,放入一些更小的类中,而这些类只是真正读取文件系统的操作,从而方便对这个类以及可能涉及到的其他类进行测试。 - ColinD

2

您可以传入一个自定义的ClassLoader,当调用时提供tocUrl的测试实例。然而,为什么要传入ClassLoader呢?如果您只使用tocUrl,那么只需传入它而不是ClassLoader,并将其存根化。这大大简化了事情。

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

这里的问题在于你的构造函数既要执行任务,又要设置状态。为了可测试性,你真的需要将这两个任务分开。你可以看到,现在所有的东西都混杂在一起了。


2

单元测试只是你测试工具箱中的一个工具。但像所有工具一样,它有一个既定的目的和有限的适用范围。罗伊·奥舍罗夫的《单元测试之道》将解释当涉及到外部依赖时,单元测试并不适合。这是因为在此问题的其他地方已经说明了原因:保持测试运行速度,使测试在开发人员环境中可重复,消除错误的测试失败等。

从文件系统中读取,即使这是代码的一个重要工作,也是一种外部依赖。因此,在我看来,你正在试图强制这种情况成为可进行单元测试的情况,而实际上并非如此。你应该对可以进行单元测试的内容进行单元测试——使用模拟库和良好的解耦设计——并使用其他测试工具(例如手动或自动化集成测试)来测试外部依赖。


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