【问题标题】:How to keep Unit tests DRY and reducing Asserts如何保持单元测试干燥并减少断言
【发布时间】:2015-11-18 18:15:13
【问题描述】:

我正在尝试使用 TDD 方法实现一个 webserviceclass,它发出一堆 web 请求并解释响应。我将 webrequests 封装在几个接口中,以便我可以轻松地模拟它们。当通过 webserviceclass 请求某些东西时,实现的方法总是返回一个包含错误对象的特定响应对象。借助此错误对象,用户可以确定请求是否成功,以及具体的错误是什么。

在写了一堆测试之后,我意识到我在 Arrange 阶段重复了很多次:

var mock = new Mock<ISomeWebservices>();
var sut = new MyWebServiceClass(mock.Object);
mock.Setup(foo=>foo.SomeRequest(someData)).Returns(@"{""user"": ""12345"",""somedata"": ""60"",""someotherdata"":""2015-09-01T12:00:00.200Z""}");
sut.SomeRequest(someData,s=> response = s);

所有测试的前 2 行始终相同。总是有一个模拟的设置,它返回一些东西或抛出一个异常。根据我正在测试的请求,我当然必须设置不同的方法。 我尝试使用 Autofixture 来解决这个问题,这样我就可以为每个 webrequest 编写一个 ICustomization,但问题是我仍然需要访问模拟来进行测试特定设置。

另一个问题是断言阶段。因为我总是从请求中得到一个错误对象,所以我只在预期和错误时断言错误对象。

    Assert.That(response.Error.Type,Is.EqualTo(ErrorInfo.ErrorType.IllegalToken));
    Assert.That(response.Error.Message, Is.Not.Null);
    Assert.That(response.Error.AdvisedAction, Is.Not.Null);
    Assert.That(response.Error.RawData, Is.Not.Null);

因此,如果出现错误,则错误对象的某些属性应始终填写,而有些则可以。 (例如,如果错误是由异常触发的,则异常属性不应为 null 等) 据我了解,在一个单元测试中使用多个断言是一种不好的做法,因此我希望尽可能避免这种做法。

[编辑] 根据评论,拥有多个断言以及重复我提到的排列部分并不是那么糟糕。所以我不会为此使用 Autofixture。

【问题讨论】:

  • 干性在编写单元测试时并不重要,因为它会导致脆弱的测试codebetter.com/karlseguin/2009/09/12/…
  • 我不认为几个 Asserts 不好。测试不同的、不相关的东西是不好的。但是取回一个对象并测试属性是否正确对我来说是可以的。
  • 对于你的sut,你不能有类变量,并使用Setup方法创建它吗?

标签: c# tdd nunit moq autofixture


【解决方案1】:

您当然可以使用 AutoFixture 来减少您在 Arrange 阶段所需的样板代码量。它可能看起来像这样:

[Theory, AutoMoqData]
public void Test(
    [Frozen]Mock<ISomeWebservices> mock,
    MyWebServiceClass sut,
    object someData,
    object response)
{
    mock.Setup(foo => foo.SomeRequest(someData)).Returns(@"{""user"": ""12345"",""somedata"": ""60"",""someotherdata"":""2015-09-01T12:00:00.200Z""}");
    sut.SomeRequest(someData, s => response = s);
    // Assertions go here...
}

在这里,我不得不猜测someDataresponse 的类型,但如果它们与object 不同,您只需将它们声明为该类型即可。

在上面的例子中,[AutoMoqData] 属性是这样定义的:

public class AutoMoqDataAttribute : AutoDataAttribute
{
    public AutoMoqDataAttribute() :
        base(new Fixture().Customize(new AutoMoqCustomization()))
    {
    }
}

所有其他类型和 AutoFixture 功能均来自 AutoFixture.Xunit2AutoFixture.AutoMoq NuGet 包。

当涉及到多个断言时,我同意这里其他评论者的观点,即看起来并没有那么糟糕,但本着GOOS 的精神,您应该听取您的测试。在这种特殊情况下,测试似乎在说:response 的平等比较看起来很重要。 如果是这种情况,您可以考虑在 response 上覆盖 Equals 并给出它结构平等而不是引用平等

【讨论】:

  • 感谢您的详细解释,这正是我想要的。但是我使用的是 Nunit 而不是 Xunit,但我不明白为什么这不适用于 AutoFixture.Nunit2 包?当我尝试这个时,我得到一个 System.Reflection.TargetParameterCountException。
  • @zlZimon NUnit 2 的可扩展性模型非常混乱,如果你能让它工作,你会很幸运。 AutoFixture.NUnit2 是将 AutoFixture.Xunit2 移植到 NUnit 的最大努力尝试,但是 AFAICT,它非常不稳定。帮自己一个忙,改用 xUnit.net。从 NUnit 迁移到 xUnit.net 很容易。
  • 我听从了您的建议并迁移到了 XUnit2,我真的很喜欢它。不幸的是,我大量使用 NUnit 的类别,但那是另一回事。我想知道始终初始化 sut/mocks/ 或通过 Theory 属性在测试中使用的任何数据是否是一种好习惯?我真的很喜欢我的测试现在看起来多么干净。
  • 所以你永远不会写像var sut = new mySut();这样的东西?我真的很喜欢。
  • @zlZimon 会发生,但并不经常发生。
【解决方案2】:

您希望尽可能避免重复,最好按照生产代码标准编写测试代码,因为您可以花与生产代码一样多的时间来阅读和维护测试代码。取得平衡是关键,因为您不想像所指出的那样需要脆弱的测试,而是相互依赖。

在创建测试数据和对象时,通常使用测试数据构建器来简化和消除重复。如果您更改对象的创建方式,这也会有所帮助,因为您只更新一个构建器方法而不是所有单独的测试。

每个测试有多个断言很好(可能是必要的),只要确保断言是相关的并且测试坚持一个概念,有助于快速识别错误。

【讨论】:

    猜你喜欢
    • 2017-01-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-08
    相关资源
    最近更新 更多