【问题标题】:Is it possible to reuse code for integration and unit tests?是否可以重用代码进行集成和单元测试?
【发布时间】:2012-07-23 22:36:33
【问题描述】:

我使用具有单元和集成测试的分布式系统。我试图通过在集成和单元测试之间重用代码来节省时间和维护工作。为此,我实现了一个接口和 2 个类:fake 和 real。 Fake 类返回一些存根数据,real 类对其他分布式服务进行几次调用。

我的项目的当前结构

/BaseTest 接口 IFoo ------------------------------------- /单元测试 FakeFoo 类:IFoo [测试夹具] class FooTest {...} //使用 FakeFoo ------------------------------------- /集成测试 类 RealFoo : IFoo [测试夹具] class FooTest {...} //使用 RealFoo

我想以某种方式重用两个测试的代码,所以如果我有一个测试

[Test]
public void GetBarIsNotNullTest()
{
    var foo = IoC.Current.Resolve<IFoo>();
    Bar actual = foo.GetBar();
    Assert.IsNotNull(actual);   
}

我希望这个测试在两个实现中运行:RealFooFakeFoo。到目前为止,我考虑过在 /UnitTest/IntegrationTest 项目之间进行复制粘贴测试,但这听起来不对。

系统是用 C# 编写的,但我相信这个问题与语言无关。

谁有更好的主意?我做错了吗?

【问题讨论】:

  • 为什么不将测试内容移动到共享库中,然后从单独的测试中调用它?
  • 我认为你是对的——请参阅下面的我的 cmets 来了解你的答案;我的问题是如何让它与常规的 MS 测试框架 (vs2012) 一起使用?

标签: c# unit-testing language-agnostic tdd integration-testing


【解决方案1】:

您的测试场景有一些非常错误的地方。

让我们先看看单元测试。您有一个可以提供可预测结果的依赖存根。您有 CUT,它应该根据存根配置给出 expected 结果。到目前为止一切顺利。

但是。如果您想在集成测试中重用您的测试代码(您的断言),这实际上意味着您希望您的 真正的依赖实现 产生与您在单元测试中存根所做的相同的结果。如果是这样,您为什么不直接测试您的依赖关系以给出这些结果并跳过整个代码层?

更新

你的例子是错误的。 FakeFoo 是一个存根,应该对其进行测试。您使用存根来测试依赖某些服务的类。所以让我们假设您正在测试一些依赖于IFoo 的类Bar,这意味着:

[Test]
public void GetBarIsNotNullTest()
{
    var bar = IoC.Current.Resolve<Bar>();
    var actual = bar.GetDon();
    Assert.IsNotNull(actual);   
}

并且您在测试中使用了不同的 IFoo 实现。

澄清我的立场

因为您在测试中复制了 Act 和 Assert 阶段,所以您很可能在 CUT (Bar) 中测试相同的代码路径。这意味着测试重复,并不比代码重复好。

您应该确保您的 CUT (Bar) 在所有代码路径上都使用假货(这将是单元测试)。然后你应该确保你的 dependency (RealFoo) 返回预期的数据(这将是集成测试,因为它适用于分布式服务)。无需使用RealFoo 测试Bar,因为它已经过全面测试。

【讨论】:

  • 我不一定期望结果完全相同。例如,一个操作应该返回一个具有某些特性的对象,例如 not-null。在这种情况下我该怎么办?我是否正确,您建议只进行集成测试?我可能没听懂。
  • 我谈论的是断言,而不是结果。当您针对“某些品质”测试您的操作时,这是一个断言。当您有两个具有相同 Act 和 Assert 的测试时,您正在复制您的代码。我会在帖子中澄清我的提议。
  • "你应该确保你的 CUT (Bar) 在所有代码路径上都是好的,通过使用 fakes (这将是单元测试)。然后你应该确保你的依赖 (RealFoo) 返回预期数据(将是集成测试......)”这种分层方法的问题是您的集成测试可能通过(断言匹配服务数据)并且您的单元测试可能通过(断言匹配模拟数据),但服务数据可能与模拟数据不匹配。
  • 这两个测试层中的预期数据很容易不同步,因此即使您正在重新测试零件,从头到尾进行测试也是有价值的。将其称为集成测试、端到端测试或其他任何东西。将集成测试和单元测试分开的部分原因是集成测试需要很长时间。您希望单元测试快速,以便开发人员经常测试。集成测试可能只在 CI/build 机器上运行。作为更大的集成测试的一部分重新测试您的“单元测试”不会增加大量时间,并会确保模拟和真实数据同步。
【解决方案2】:

编写自己的虚假实现不会节省您的时间。创建依赖模拟要容易得多:

Mock<IFoo> fooMock = new Mock<IFoo>();

最糟糕的部分是设置你的模拟对象。您可以为不同的测试场景设置不同的结果:

fooMock.Setup(f => f.Bar).Returns(true);
// or
fooMock.Setup(f => f.Bar).Returns(false);

这对于假货是不可能的。您将拥有Bar 属性的一个实现,它将返回truefalse

更新:单元测试和集成测试之间的唯一区别是测试的Arrange 部分。 ActAssert 可能相同(如果您只进行状态测试,而不进行交互测试)。但是Arrange 完全不同。它不仅仅是创建直接依赖的实例。如果您使用模拟,您应该为 SUT 使用的成员设置返回结果。这足以重现测试场景。但对于真实对象,您应该将堆栈中的所有依赖项设置为 当前 测试场景所需的某种状态。

【讨论】:

  • Mocks 很好,但是如果我使用 mocks,我将需要重新编写集成测试的代码,这就是我想要逃避的。对于某些测试,我确实使用了 mock,但 fake 更容易重复使用。
  • 再一次,你不能使用 fakes 进行单元测试,因为你不能为存根成员设置不同的返回结果。
  • 关于您的更新,您说唯一不同的是Arrange。这是正确的。我通过 DI 和 IoC 安排,因此这是一个单独的问题,一旦测试解决了正确的依赖关系,所有其他步骤都是相同的。此外,Resolve 步骤是相同的​​,因为我使用接口。就我而言,唯一不同的是SetUp,所有依赖项都在其中注册
  • 排列不仅仅是一部分(参见斜体)。查看我对您帖子的问题:)
【解决方案3】:

尽管其他人的回答很好,但我最终还是这样做了

我为单元测试和集成测试创建了一个基类

[TestFixture]
public class FooBase
{
    [Test]
    public void GetBarIsNotNullTest()
    {
        var foo = IoC.Current.Resolve<IFoo>();
        Bar actual = foo.GetBar();
        Assert.IsNotNull(actual);   
    }

    //many other tests  
}

然后是FooBase 的两个派生类。这些类只有SetUp,没有别的。即:

[TestFixture]
public class UnitTestFoo : FooBase
{
    [SetUp]
    public void SetUp()
    {
        IoC.Current.Register<IFoo, FakeFoo>();        
    }

    //nothing else here
}

[TestFixture]
public class IntegrationTestFoo : FooBase
{
    [SetUp]
    public void SetUp()
    {
        IoC.Current.Register<IFoo, RealFoo>();        
    }

    //nothing else here
}

所以如果我现在运行我的测试,我会在父类 FooBase 中定义的测试运行两次,分别用于单元测试类和集成测试类,它们有自己的 realfake 对象。这是因为测试装置的继承性。

【讨论】:

  • 你在哪里填写数据库表,RealFoo 需要,或者RealFoo 使用的RealRepository
  • 你将如何测试场景,foo.GetBar() 返回 null?
  • @lazyberezovsky 目前我使用的是手动启动并从测试中填充的内存中 RavenDB 实例。我仍在研究如何实现自动化。对于您的第二点,在我的情况下,这将是特殊情况,测试必须失败。我也确实使用模拟进行测试,只是有一个地方假货更好。
  • 好的,您的容器如何解析当前场景的数据库中应该包含哪些记录?模拟也是一样。您的 DI 容器设置如何针对每个单独的测试进行不同的模拟?
  • @lazyberezovsky 这就是问题所在。如果我需要不同的场景,我会使用模拟和单独的集成测试。但如果单元测试和集成测试的场景相同,这就是我使用 IoC 和 fakes 的地方。我的 IoC 不涉及模拟,因为我认为这是不可能的(或者看起来真的很奇怪)。
猜你喜欢
  • 1970-01-01
  • 2020-02-21
  • 1970-01-01
  • 2016-02-06
  • 1970-01-01
  • 2017-09-27
  • 2023-03-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多