单元测试以测试域对象的创建


11

我有一个单元测试,如下所示:

[Test]
public void Should_create_person()
{
     Assert.DoesNotThrow(() => new Person(Guid.NewGuid(), new DateTime(1972, 01, 01));
}

我声称这里创建了一个Person对象,即验证不会失败。例如,如果Guid为null或生日早于01/01/1900,则验证将失败并且将引发异常(意味着测试失败)。

构造函数如下所示:

public Person(Id id, DateTime dateOfBirth) :
        base(id)
    {
        if (dateOfBirth == null)
            throw new ArgumentNullException("Date of Birth");
        elseif (dateOfBith < new DateTime(1900,01,01)
            throw new ArgumentException("Date of Birth");
        DateOfBirth = dateOfBirth;
    }

这是测试的好主意吗?

注意:我遵循经典方法对单元模型进行单元测试(如果有)。


构造函数是否有任何值得在初始化后声明的逻辑?
拉夫

2
永远不要打扰测试构造函数!!!施工应直截了当。您是否期望Guid.NewGuid()或DateTime的构造函数失败?
ivenxu

@Laiv,请参阅问题的更新。
w0051977

1
与您共享的测试相比,实施一项测试毫无价值。但是,我也会进行相反的测试。我会测试birthDate导致错误的情况。这就是您想要成为受控制和测试的类的不变性。
Laiv

3
测试看起来不错,除了一件事:名称。Should_create_person?什么应该创造一个人?给它起一个有意义的名称,例如Creating_person_with_valid_data_succeeds
David Arno

Answers:


18

这是一个有效的测试(尽管相当狂热),我有时会这样做以测试构造函数的逻辑,但是正如Laiv在评论中提到的那样,您应该问自己为什么。

如果您的构造函数如下所示:

public Person(Guid guid, DateTime dob)
{
  this.Guid = guid;
  this.Dob = dob;
}

测试是否有很多意义吗?参数是否正确分配我可以理解,但是您的测试相当过头。

但是,如果您的测试执行以下操作:

public Person(Guid guid, DateTime dob)
{
  if(guid == default(Guid)) throw new ArgumentException("Guid is invalid");
  if(dob == default(DateTime)) throw new ArgumentException("Dob is invalid");

  this.Guid = guid;
  this.Dob = dob;
}

然后您的测试变得更加相关(因为您实际上是在代码中的某个地方抛出异常)。

我要说的一件事,通常是在构造函数中包含很多逻辑是一种不好的做法。基本验证(例如我在上面进行的null /默认检查)是可以的。但是,如果您要连接到数据库并加载某人的数据,那么这就是代码开始真正散发出来的地方...

因此,如果您的构造函数值得测试(因为要进行大量逻辑处理),则可能还有其他问题。

几乎可以肯定,您将在业务逻辑层中有涉及该类的其他测试,构造函数和变量赋值几乎肯定会从这些测试中获得完整的介绍。因此,可能没有必要添加专门针对构造函数的特定测试。但是,没有什么是黑白的,如果我通过代码审查它们,我不会反对这些测试-但是我想知道它们是否比解决方案中其他地方的测试有更多的价值。

在您的示例中:

public Person(Id id, DateTime dateOfBirth) :
        base(id)
    {
        if (dateOfBirth == null)
            throw new ArgumentNullException("Date of Birth");
        elseif (dateOfBith < new DateTime(1900,01,01)
            throw new ArgumentException("Date of Birth");
        DateOfBirth = dateOfBirth;
    }

您不仅要进行验证,还要调用基本构造函数。对我来说,这提供了进行这些测试的更多理由,因为它们现在将构造函数/验证逻辑分为两个类,这降低了可见性并增加了意外更改的风险。

TLDR

这些测试具有某些价值,但是解决方案中的其他测试可能涵盖验证/分配逻辑。如果这些构造函数中有很多逻辑需要进行大量测试,那么对我来说,这表明其中隐藏着令人讨厌的代码气味。


@Laith,请查看我的问题更新
w0051977

我注意到您在示例中正在调用基本构造函数。恕我直言,这会增加测试的价值,构造函数逻辑现在分为两个类,因此更改风险略高,因此有更多理由对其进行测试。
Liath

“但是,如果您的测试做了这样的事情:” <您不是说“如果您的构造函数做了这样的事情”吗?
科多斯·约翰逊

“这些测试有一些价值”-无论如何,对我而言有趣的是,该价值表明我们可以通过使用一个新类来表示PersonBirthdate执行出生日期验证的人的Dob(例如)来使该测试变得多余。同样,Guid可以在Id类上执行检查。这意味着您真的不再需要在Person构造函数中使用该验证逻辑,因为不可能用无效数据构造一个验证逻辑- null引用除外。当然,您必须为其他两个类编写测试:)
Stephen Byrne,

12

这里已经是一个很好的答案,但是我认为还有一件事值得一提。

当“按书进行” TDD时,甚至在构造函数实现之前,都需要先编写一个调用该构造函数的测试。该测试实际上看起来像您提出的测试,即使构造函数实现中的验证逻辑为零。

另请注意,对于TDD,您应该先编写另一个测试,例如

  Assert.Throws<ArgumentException>(() => new Person(Guid.NewGuid(), 
        new DateTime(1572, 01, 01));

将检查添加DateTime(1900,01,01)到构造函数之前。

在TDD上下文中,所示的测试非常有意义。


我不会考虑好的角度!
Liath

1
这向我展示了为什么如此严格的TDD形式会浪费时间:在编写代码之后测试应该具有价值,或者您只是在每一行代码中编写两次,一次作为断言,一次作为代码。我认为构造函数本身不是需要测试的逻辑;业务规则“不能在1900年之前出生的人不能代表”是可以测试的,而构造函数正是在该规则的实现位置进行的,但是什么时候对空构造函数进行测试才能为项目增加价值?
IMSoP

这本书真的很有趣吗?我将创建实例并立即在代码中调用其方法。然后,我将为该方法编写测试,然后,我还必须为该方法创建实例,因此该测试中将同时涉及构造函数和方法。除非在构造函数中有一些逻辑,否则该部分将由Liath涵盖。
拉斐尔

@RafałŁużyński:TDD“按书”是关于首先编写测试。实际上,这意味着始终要首先编写失败的测试(也不要将计数也视为失败)。因此,即使没有构造器,您也要编写一个调用构造器的测试。然后尝试进行编译(失败),然后实现一个空的构造函数,进行编译,运行测试,结果=绿色。然后,编写第一个失败的测试并运行它-result = red,然后添加使测试再次“绿色”的功能,依此类推。
布朗

当然。我并不是说我先编写实现,然后再测试。我只是在上一级编写该代码的“用法”,然后对该代码进行测试,然后实现它。我通常在做“外部TDD”。
RafałŁużyński
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.