使用File.OpenRead()进行类的单元测试

3

我希望测试一个使用 File.OpenRead() 方法获取文件内容的类。读取文件内容后,该类会处理这些内容。我已经创建了一个接口和一个包装静态 OpenRead() 方法的类。但是,我遇到了一个问题,即 OpenRead() 返回一个 FileStream,我不知道如何“模拟”文件流。

目前,我只能创建一个文件来创建一个 FileStream... 当然,测试通常会因为文件仍在使用中而失败并抛出 IOException 异常...

简化的示例代码如下:

class FileProcessor
{
  public FileProcessor(IFileWrap fileWrap) // fileWrap only redirects the calls to the static methods of File class
  { ... }

  public void Process(string file)
  {
    var content = fileWrap.ReadAllLines(file);

    // process content
  }
}

并且测试:

[TestClass]
public class FileProcessor_Test
{
  [TestMethod]
  Process_FileNotReadable_Exception()
  {
    File.WriteAllText(testFile, "something");
    var fileWrapMock = new Mock<IFileWrap>();
    FileProcessor dut = new FileProcessor(fileWrapMock.Object);
    var actualException = AssertException.Throws<Exception>(() => dut.Process(testFile));
  }
}

我希望也不用创建一个 FileStream 的抽象。
我曾希望能够创建一个 MemoryStream 并将其作为输入的方式,但这将需要更改文件包装器并偏离实际的 File 类。
欢迎提供任何意见 :)
编辑: 处理过程包括通过从类 MD5 调用 ComputeHash() 来计算 MD5 校验和。
3个回答

4

你原本的方法与其操作文件紧密耦合。不要这样做。让该方法接受一个Stream,任何流都可以。你可以操作一个FileStream,或者传递一个MemoryStream进行测试。

public void Process(string file)

应该是

public void Process(Stream stream)

如果您愿意,可以为方便起见添加第二个方法重载:
public void Process(string file)
{
    using (var stream = new FileStream(file, FileMode.Open))
    {
        this.Process(stream);
    }
}

确实不能也不应该进行单元测试...它是处理外部资源的.NET代码,有些时候你必须信任框架。


针对您的编辑:大多数框架类都会做类似的事情,例如MD5类有一个ComputeHash方法可用于流。


我在考虑将类更改为在Stream上操作,但我认为这只会将创建流的问题暂时转移到另一个地方...难道我不需要检查如果流创建失败会发生什么,例如由于文件访问错误?那么我不是要测试如何处理这些问题吗? - royalTS
如果您编写一个集成测试(而不是单元测试)- 您需要注意这个问题。 - Leonid
你确定一个单元测试不应该检查流创建异常的处理吗?即使它只是将异常包装成另一个异常并添加了更多信息。 - royalTS
1
@royalTS 如果你愿意的话,你可以为上述方法编写一个不存在文件的单元测试。我只是想说,文件处理不再是一个单元测试,因为它操作的不仅仅是它的单元。而且你需要一种方式来编写所有那些不需要实际访问文件系统就能通过的测试,这就是流参数所提供的功能。 - nvoigt
1
@nvoigt 谢谢!我会将这个类更改为在流上操作,任何使用它的高级用户都将负责创建流。 - royalTS

2

首先,您不需要对“File”类进行单元测试,因为“OpenRead”是“File”类的静态方法,您无法解耦。针对具有依赖关系的任何内容编写单元测试都不理想。在您的情况下,您可以模拟“IFileWrap”并创建“ReadAllLines”的模拟方法。当单元测试命中“fileWrap.ReadAllLines(file)”方法时,它将调用模拟方法而不是访问“File.OpenRead()”方法。


实际上,IFileWrap 已经包含了 ReadAllLines() 方法的定义。事实上,我可以使用它来获取文件的内容。 - royalTS
但是你的单元测试不应该涉及到 'ReadAllLines()' 的定义,因为这会依赖于 'File.OpenRead()',而后者会访问你的实际文件,这不是一个单元测试,而是一个集成测试。单元测试不应该有任何依赖关系。 - Vijayanath Viswanathan

1
我建议您将IFileWrap改为返回字符串(读取所有行),而不是MemoryStream。基本上,可以创建一个封装文件的抽象层。

IFileWrap 返回一个 FileStream,就像 File.OpenRead() 一样。我希望在我的测试中创建一个 MemoryStream 并将其作为 FileStream 提供。 - royalTS
如果您需要文件的内容,您可以创建一个返回此内容(字符串或byte [])的接口,并在内部实现中使用流进行操作。这可以显着简化您的测试。 - Leonid

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