TDD - 我这样做了吗?

时间:2014-09-24 03:11:30

标签: c# .net tdd moq

我有一个处理帐户内容的课程。它提供了登录,重置密码和创建新帐户的方法。

我通过构造函数注入依赖项。我有测试验证每个依赖项的引用,如果引用为null,则抛出ArgumentNullException。

Account类通过只读属性公开每个依赖项,然后我有测试验证构造函数上传递的引用是否与属性返回的相同。我这样做是为了确保班级持有引用。 (我不知道这是不是一个好习惯。)

第一个问题:这是TDD的一个好习惯吗?我问这个是因为这个类到目前为止有6个依赖项,并且它变得非常重复,而且测试也很长,因为我必须模拟每个测试的所有依赖项。我所做的只是每次复制和粘贴,只需更改正在测试的依赖项参考。

第二个问题:我的帐户创建方法执行的操作包括验证传递的模型,在3个不同的表或第4个表中插入数据(如果存在某组值并发送电子邮件)。我应该在这里测试什么?到目前为止,我有一个测试,检查模型验证是否被执行,如果每个存储库的Add方法被调用,在这种情况下,我使用模拟存储库的Moq的Callback方法来比较添加到存储库的每个属性我通过模特的那些人。

类似的东西:

    userRepository
        .Setup(r => r.Add(It.IsAny<User>()))
        .Callback<User>(u =>
            {
                Assert.AreEqual(model.Email, u.Email);
                Assert.IsNotNull(u.PasswordHash);
                //...
            })
        .Verifiable();

正如我所说,这些测试越来越长,我认为测试任何东西都没有坏处,但我不知道它是否值得,因为它需要时间来编写测试。

3 个答案:

答案 0 :(得分:4)

测试的目的是发现错误。

您是否真的会遇到属性存在但未初始化为构造函数值的错误?

public class NoNotReally {
    private IMyDependency1 _myDependency;
    public IMyDependency1 MyDependency {get {return _myDependency;}}

    public NoNotReally(IMyDependency dependency) {
        _myDependency = null; // instead of dependency. Really?
    }
}

此外,由于您使用的是TDD,因此您应该在编写代码之前编写测试,并且代码应该只存在以使测试通过。而不是对属性进行不必要的测试,编写一个测试来演示您正在使用注入的依赖项。在顺序或这样的测试中,依赖性需要存在,它需要是正确的类型,并且需要在特定的场景中使用。

在我的例子中,依赖关系将会存在,因为它是必需的,而不是因为某些人工单元测试需要它存在。

答案 1 :(得分:3)

你说写这些测试感觉重复。我说你觉得TDD的主要好处。实际上,这不是编写具有较少错误的软件而不是编写更好的软件,因为TDD也不能保证(至少不是固有的)。 TDD迫使您考虑设计决策并做出所有设计决策。的。时间。 (并减少调试时间。)如果你在做TDD时感到疼痛,通常是因为设计决定会回来咬你。然后是时候切换到你的重构帽并改进设计。

现在在这种特殊情况下,它只是测试的设计,但你必须为这些设计做出设计决定。

至于测试是否设置了属性。如果我理解正确,你只是为了测试而暴露这些属性?在那种情况下,我建议不要这样做。假设你有一个带有构造函数参数的类,并且有一个测试断言construtor应该抛出null参数:

public class MyClass
{
    public MyClass(MyDependency dependency)
    {
        if (dependency == null)
        {
            throw new ArgumentNullException("dependency");
        }
    }
}

[Test]
public void ConstructorShouldThrowOnNullArgument()
{
    Assert.Catch<ArgumentNullException>(() => new MyClass(null));
}

(省略TestFixture类)

现在,当您开始为正在测试的类的实际业务方法编写测试时,这些部分将开始组合在一起。

[Test]
public void TestSomeBusinessFunctionality()
{
    MyDependency mockedDependency;

    // setup mock
    // mock calls on mockedDependency

    MyClass myClass = new MyClass(mockedDependency);

    var result = myClass.DoSomethingOrOther();

    // assertions on result
    // if necessary assertion on calls on mockedDependency
}

此时,您必须将构造函数中注入的依赖项分配给字段,以便稍后在方法中使用它。如果你设法让测试通过而不使用依赖...好吧,哎呀,显然你开始时并不需要它。或者,也许,您只是开始需要它进行下一次测试。

关于另一点。当测试方法或类的所有可复制性变得麻烦时,TDD告诉您方法/类正在做很多事情并且可能希望被分成易于测试的部分。例如。一个用于验证的类,一个用于映射,一个用于执行存储调用。

但是,这很可能导致过度工程化!所以要注意这一点,你会产生一种感觉,何时抵制更多间接的冲动。 ;)

为了测试属性是否正确映射,我建议使用具有简单属性的存根或自制虚假对象。这样您就可以简单地比较源属性和目标属性,而不必像您发布的那样进行冗长的设置。

答案 2 :(得分:1)

通常在单元测试中(特别是在TDD中),您不会测试您正在测试的类中的每个语句。 TDD单元测试的主要目的是测试类的业务逻辑,而不是初始化的东西。

换句话说,您给出方案(记住也包括边缘情况)作为输入并检查结果,结果可以是属性的最终值和/或方法的返回值。

您不希望在类中测试每个可能的代码路径的原因是因为如果您决定稍后重构您的类,您只需对TDD单元测试进行最小的更改,因为它们应该是与实际实施无关(尽可能多)。

注意:其他类型的单元测试,例如代码覆盖率测试,用于测试类中的每个代码路径。但是,我个人认为这些测试不切实际,在TDD中肯定不会鼓励。