【问题标题】:TDD: Is it plausible to have integration tests, but no unit tests?TDD:进行集成测试但没有单元测试是否合理?
【发布时间】:2012-02-15 16:50:14
【问题描述】:

技术栈:.NET 4、C#、NUnit

我正在尝试将测试驱动开发应用于执行图像处理的新项目。我有一个基类,其中包含共享文件 I/O 方法和执行各种特定处理算法的子类。据我了解,单元测试不会触及文件系统或其他对象,并模拟发生这种情况的行为。我的基类只包含简单的访问器和直接的文件系统 I/O 调用。

public class BaseFile
{
    public String Path { get; set; }

    public BaseFile()
    {
        Path = String.Empty;
    }

    public BaseFile(String path)
    {
        if (!File.Exists(path))
        {
            throw new FileNotFoundException("File not found.", path);
        }

        Path = path;
    }
}

测试这些方法有什么价值吗?如果是这样,我该如何抽象出对文件系统的调用?

我的另一个问题是如何测试特定于一种图像文件(~200 MB)的子类。我搜索了该站点并找到了similarquestions,但没有人处理我在这个项目中使用的文件大小。一个类是否有可能进行集成测试(使用“黄金文件”),但没有单元测试?在这种情况下,我怎样才能严格遵循 TDD 方法并首先编写一个失败的测试?

【问题讨论】:

  • 如果 TDD 难以应用或不足,请不要应用它。这不是灵丹妙药。
  • @CharlesB,我同意。不幸的是,这种情绪经常被用作不使用 TDD 的借口,而这样做实际上是正确和有利的。有时有很大的曲线或大量的基础工作要做,但这通常是有回报的。
  • @CharlesB 不确定我是否同意。问题是这个做一些工作的类没有经过测试,因此你改变它的信心就会减弱。问题是在 System.IO 中模拟静态方法并不容易,但这并不意味着放弃测试,只是意味着您需要做更多的工作才能使它们可测试。这就是 MS 引入 System.Web.HttpContextBase 的原因,以解决无法轻松模拟 HttpContext 的问题。我们不需要测试 httpContext,只要我们的代码正确地与之交互即可。
  • @CharlesB - TDD 是一门学科。它并不是真的要解决任何一个问题。它的作用是迫使您对代码应用干净的边界,并将自己与实现细节分离。作为副作用......你会得到一组测试。

标签: c# .net unit-testing nunit


【解决方案1】:

在回答您的第一个问题时,是的,测试这些方法是有价值的。我已经发布了一个库,可以在不影响文件系统的情况下准确地做到这一点:https://bitbucket.org/mylesmcdonnell/mpm.io/wiki/Home

在(不是)回答您的第二个问题时,我需要查看一些代码,但我怀疑您可能需要对上述库采取类似的方法。即;定义接口,定义代理到具体,定义工厂返回代理或模拟。

【讨论】:

  • 感谢您发布此源代码。我将您的帖子标记为答案,因为您提供了最佳的通用解决方案。
【解决方案2】:

我怎样才能严格遵循 TDD 方法并首先编写一个失败的测试 在这种情况下?

简单!你模拟文件系统:)

这听起来像是很多工作,但通常您只需要实现几个方法,并根据需要进行扩展。在上述情况下……您只需要一个。

public interface IFileStore
{
    Boolean FileExists(String path);
}

这个想法是将您的文件工作委托给接口,并创建一个具体的实现来完成繁重的工作。基本上是Adapter pattern。

我什至不反对这种事情的“穷人的 DI”,因为如果您的应用程序需要它,您可以稍后实现一个容器。在这种情况下……您可能总是会使用真实的文件系统。

public class BaseFile
{
    private IFileStore _fileStore
    public IFileStore FileStore
    {
        get
        {
            return _fileStore ?? (_fileStore = new ConcreteFileStore());
        }
        set
        {
            _fileStore = value;
        }
    }

    //SNIP...
}

现在您有了一个可测试的实现,您不必依赖任何“黄金”文件。

【讨论】:

  • 感谢您的帖子。现在,我将实施您的解决方案,直到我需要更多 System.IO 调用,此时我将使用 Myles 的代码。
【解决方案3】:

测试这些方法很有价值

虽然现在看起来额外的工作是微不足道的,但添加像 BaseFile 这样的测试应该在文件不存在时抛出 FileNotFoundException 至少可以实现两个目标:

定义预期行为列表

项目的新手可以查看测试名称以确定您的类的预期操作方式。他们将知道在每种情况下会发生什么——异常、空值、默认结果等。

它还迫使您仔细思考并用简单的英语定义您希望事情如何运作,而不是仅仅在这里和那里抛出条件和异常。这应该会导致在您的项目中应用非常一致的理念。

开发一套自动化回归测试

假设有人看到一些代码在特定条件下抛出异常,但他们认为做其他事情更明智(吃掉错误,但添加一个新的 IsValid 属性,以便消费者可以知道构造/初始化是否成功)。如果他们做出这样的改变,测试将很快引起人们对改变的关注。事情的发展方式背后有一个有意识和有意的决定,人们可能已经逐渐依赖现有的行为——这种变化需要进一步讨论才能被接受。

至于你问题的第二部分,我认为 Josh 和 Myles 都已经提供了合理的建议。

【讨论】:

  • 你的观点很好。我相信我会留在这些简单方法的测试中。
【解决方案4】:

使用接口模拟简单的文件系统调用似乎有点过头了。使用 ITimeService 模拟当前时间之类的事情也是如此。我倾向于使用 Func 或 Action,因为它更简单:

public static Func<string, bool> FileExists = System.IO.File.Exists;
public static Func<DateTime> GetCurrentTime = () => DateTime.Now;

由于这些是公开的,我可以在单元测试中轻松地模拟它们。代码保持简单,无需为简单的事情注入各种接口。对于流,我通常在单元 tets 中使用 MemoryStream。

【讨论】:

    【解决方案5】:

    集成测试本身就有价值。如果您模拟文件系统,就像 Joshs answer 中解释的那样,您真的不确定您的代码是否会在生产中实际运行。文件系统有很多隐藏的合同,这些合同对模拟来说并不简单。如果你的 mock/fake 表现出稍微不同的行为,你的代码可能会在你不知情的情况下开始依赖它。

    只有集成测试才能确定地说明某些事情。 (集成测试也有缺点!)。

    【讨论】:

    • 正确。我并不是暗示您不应该进行集成测试。相反,即使在我们通常不考虑的东西(例如文件系统)上,TDD 仍然可以完成。
    • 我不是说贬低你的解释。我引用它是因为它很好。
    猜你喜欢
    • 1970-01-01
    • 2013-09-30
    • 1970-01-01
    • 1970-01-01
    • 2023-01-09
    • 2011-09-30
    • 1970-01-01
    • 1970-01-01
    • 2013-07-13
    相关资源
    最近更新 更多