【问题标题】:Repetitive code in unit-tests单元测试中的重复代码
【发布时间】:2012-05-16 15:15:09
【问题描述】:

我们发现自己在许多测试用例中编写重复的夹具/模拟设置 - 就像这种情况:

var fixture = new Fixture().Customize(new AutoMoqCustomization());
var encodingMock = fixture.Freeze<Mock<IEncodingWrapper>>();
var httpClientMock = fixture.Freeze<Mock<IHttpWebClientWrapper>>();
var httpResponseMock = fixture.Freeze<Mock<IHttpWebResponseWrapper>>();
var httpHeaderMock = fixture.Freeze<Mock<IHttpHeaderCollectionWrapper>>();
var etag = fixture.CreateAnonymous<string>();
byte[] data = fixture.CreateAnonymous<byte[]>();
Stream stream =  new MemoryStream(data);

encodingMock.Setup(m => m.GetBytes(It.IsAny<string>())).Returns(data);
httpHeaderMock.SetupGet(m => m[It.IsAny<string>()]).Returns(etag).Verifiable();
httpClientMock.Setup(m => m.GetResponse()).Returns(httpResponseMock.Object);
httpResponseMock.Setup(m => m.StatusCode).Returns(HttpStatusCode.OK);
httpResponseMock.SetupGet(m => m.Headers).Returns(httpHeaderMock.Object);
httpResponseMock.Setup(m => m.GetResponseStream()).Returns(stream);

根据测试应该是自包含且从头到尾可读的想法,我们不使用神奇的设置/拆卸方法。

我们能否以任何方式(AutoFixture 自定义、辅助方法)减少这些测试的“繁重工作”?

【问题讨论】:

  • 你使用哪个单元测试框架?
  • 我已经阅读了这个自成一体的概念并且根本无法同意,或者至少应该以不同的方式解释它。应将相同的生产代码维护规则应用于您的测试。
  • 您可能会觉得这很有帮助:Keep your unit tests DRY with AutoFixture Customizations

标签: c# unit-testing nunit moq autofixture


【解决方案1】:

来自Growing Object-Oriented Software (GOOS) 的一个很好的建议是:如果一个测试很难编写,它是关于被测系统 (SUT) 的 API 的反馈。考虑重新设计 SUT。在这个特定示例中,看起来 SUT 至少有四个依赖项,这可能表明违反了Single Responsibility Principle。可以refactor to Facade Services吗?

GOOS 的另一个很好的建议是

在上面的示例中,您似乎需要为真正属于查询的方法进行大量 Moq 设置。这也表明测试气味。某处是否存在Law of Demeter 违规?能不能把方法链剪掉?

【讨论】:

  • 它实际上只有一个依赖:HttpWebRequest。但是要设置模拟来模仿 HttpWebRequest(它没有任何接口,所以我们包装和包装和包装)
  • 嗯,HttpWebRequest API 很糟糕,特别是因为这个原因,但是您不能将您希望它执行的操作封装在(更高级别的)外观服务中吗?
  • 顺便说一句,根据您使用的 BCL 版本,已经为整个 API 定义了包装器:msdn.microsoft.com/en-us/library/… IOW,没有理由提出您自己的自定义包装器。但是,该 API 仍然受到 LoD 违规的严重影响。
  • 我们最终不是需要与上面完全相同的测试设置的外观服务吗?
  • 是的,您还需要对 Facade 进行单元测试,但每个单元的依赖项会更少。这将使每个测试更简单,因为您需要担心的排列会更少。这本质上是对多态性的重构。
【解决方案2】:

您可以创建一个复合自定义项,该自定义项将使用所有包含的自定义项来自定义灯具。

public class HttpMocksCustomization : CompositeCustomization
{
    public HttpMocksCustomization()
        : base(
            new AutoMoqCustomization(),
            new HttpWebClientWrapperMockCustomization(),
            new HttpWebResponseWrapperMockCustomization()
            // ...
            )
    {
    }
}

每个自定义可以定义如下:

public class HttpWebClientWrapperMockCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        var mock = new Mock<IHttpWebClientWrapper>();
        mock.Setup(m => m.GetResponse()).Returns(httpResponseMock.Object);

        fixture.Inject(mock);
    }
}

public class HttpWebResponseWrapperMockCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        var mock = new Mock<IHttpWebResponseWrapper>();
        mock.Setup(m => m.StatusCode).Returns(HttpStatusCode.OK);

        fixture.Inject(mock);
    }
}

// The rest of the Customizations.

那么在测试方法里面你可以这样做:

var fixture = new Fixture().Customize(new HttpMocksCustomization());

这样,当您请求 Mock 实例时,您不必重复设置步骤。我们之前自定义的那个会被返回:

var httpClientMock = fixture.Freeze<Mock<IHttpWebClientWrapper>>();

但是,如果您使用 xUnit.net,事情可以进一步简化。

您可以创建一个 AutoDataAttribute 派生类型,以提供 AutoFixture 生成的自动生成数据样本,作为 xUnit.net 的 Theory 属性的扩展:

public class AutoHttpMocksDataAttribute : AutoDataAttribute
{
    public AutoHttpMocksDataAttribute()
        : base(new Fixture().Customize(new HttpMocksCustomization()))
    {
    }
}

然后,在您的测试方法中,您可以将 Mocks 作为参数传递:

[Theory, AutoHttpMocksData]
public void MyTestMethod([Freeze]Mock<IHttpWebClientWrapper> httpClientMock, [Freeze]Mock<IHttpWebResponseWrapper> httpResponseMock)
{
    // ...
} 

【讨论】:

  • 此外,正如 Enrico 指出的,您可以阅读他的优秀文章 goo.gl/AFmMi,其中详细描述了自定义。
【解决方案3】:

如果您的所有测试都使用此代码,则应将其放在设置/拆卸方法中。如果您的设置/拆卸方法有些复杂,那没关系,只要您的所有单元测试都依赖于它。这肯定比在每次测试中复制所有复杂的东西要好。当我阅读测试时,我知道 setup 和 teardown 是每个测试的一部分,所以我认为你也不会失去任何可读性。要避免的事情是在设置中包含并非每个测试都需要的东西。这会造成令人困惑的情况,即您设置的方法不能很好地匹配所有测试。理想情况下,您的设置方法应 100% 应用于每个测试。

如果您的所有测试中都没有使用共享代码,请将共享代码提取到辅助函数中。编写好的测试代码就像编写任何其他好的代码一样,并且适用相同的原则。

【讨论】:

    猜你喜欢
    • 2010-12-03
    • 2012-12-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多