模拟具有纯方法和副作用的类

时间:2019-02-28 17:38:42

标签: c# unit-testing mocking moq

假设我们具有以下接口:

public interface IFileSystem
{
    string ReadFile(string filename);
    string CombinePaths(string path1, string path2);
}

以及以下具体实现

public class ConcreteFileSystem : IFileSystem
{
    public string CombinePaths(string path1, string path2)
    {
        return path1 + "/" + path2;
    }
}

ReadFile的实现在这里并不重要。

拥有文件系统实例非常好,因为它可以模拟具有副作用(例如ReadFile)的文件系统调用。

然后我们进行测试

var mock = new Mock<IFileSystem>();
var fs = mock.Object;

// How do I forward the calls to Mock<IFileSystem>.CombinePaths to ConcreteFileSystem.CombinePaths?

Assert.IsTrue(SomeClass.SomeMethodThatUsesCombinePaths("specificString1"))

在我的代码库中当前正在做的是在每次测试中都使用这样的代码:

mock.Setup(f => f.CombinePaths("specificString1", "specificString2"))
    .Returns("specificString1/specificString2");

但是由于测试应该测试接口而不是实现,因此这似乎是一种不好的方法。另外,当复制一些测试代码时,可能会出现细微的错误。


我想到的是

mock.Setup(f => f.CombinePaths(It.IsAny<string>(), It.IsAny<string>()))
    .Returns<string, string>((path1, path2) => new FileSystem().CombinePaths(path1, path2));

(也许可以使用某些C#特定语法将其缩短)。

测试的[SetUp]部分中针对IFilesystem的每种方法的这种代码不再依赖于SomeClass.SomeMethodThatUsesCombinePaths的实现。

我的问题是,这是一种好方法还是可以改进此方法。也许有一种更根本的做事方式。

2 个答案:

答案 0 :(得分:1)

我不知道您为什么要嘲笑CombinePaths。模拟是有原因的。充分的理由是:

  • 您不能轻易地使依赖组件(DOC,在这种情况下为CombinePaths)进入所需的测试状态。这不适用于这里。
  • 调用DOC是否会引起任何非专业行为(日期/时间,随机性,网络连接)?这里不是这种情况。
  • 使用原始DOC是否会导致构建/执行时间过长?可能不是。
  • 是否存在使测试不可靠的DOC稳定性(成熟度)问题,或者更糟糕的是DOC尚不可用?这里不是这种情况。

如果以上所有条件均不适用,为什么要模拟?您不必教条地嘲笑一切。例如,您也不模拟sincos之类的标准库数学函数,因为它们也没有上述任何问题。

在另一个主题上:您说“测试应该测试接口,而不是实现”。我不同意:测试最重要的是应该在代码中找到错误。这些错误在实现中。相同功能的不同实现将具有不同的错误。如果实现不重要,为什么还要关心代码覆盖率呢?当然,拥有可维护的测试和在重构的情况下不会不必要地破坏的测试是很好的目标(为什么通过公共API进行测试通常是一个好方法,但并非总是如此),但与查找所有错误的目标相比,它们是次要目标

答案 1 :(得分:0)

如果按测试原样使用实现没有副作用,请使用它们。

如果要在实现中使用的成员具有副作用,则在单独测试时应将其替换,以避免不必要的行为

模拟具体实施,启用CallBase,以便知道调用未被覆盖的实际成员

var mock = new Mock<ConcreteFileSystem>() {
    CallBase = true;
};

设置/覆盖具有副作用的成员。

mock.Setup(_ => _.ReadFile(It.IsAny<string>())
    .Returns(string.Empty); //Or what ever content you want

但是请注意,在实现中要覆盖的成员必须是虚拟的或抽象的。

public class ConcreteFileSystem : IFileSystem {
    public virtual string CombinePaths(string path1, string path2) {
        return System.IO.Path.Combine(path1, path2);
    }

    public virtual string ReadFile(string path) {
        return System.IO.File.ReadAllText(path);
    }
}