TDD-我做得对吗

本文关键字:TDD- | 更新日期: 2023-09-27 18:24:13

我有一个处理Account内容的类。到目前为止,它提供了登录、重置密码和创建新帐户的方法。

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

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

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

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

类似于:

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

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

TDD-我做得对吗

测试的目的是发现错误。

如果属性存在,但没有初始化为构造函数中的值,那么您真的会遇到错误吗?

public class NoNotReally {
    private IMyDependency1 _myDependency;
    public IMyDependency1 MyDependency {get {return _myDependency;}}
    public NoNotReally(IMyDependency dependency) {
        _myDependency = null; // instead of dependency. Really?
    }
}

此外,由于您使用的是TDD,所以您应该在编写代码之前编写测试,并且代码的存在应该只是为了使测试通过。与其对属性进行不必要的测试,不如编写一个测试来证明您正在使用注入的依赖项。为了通过这样的测试,依赖项需要存在,它需要具有正确的类型,并且需要在特定场景中使用。

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

你说写这些测试感觉重复。我说你感觉到TDD的主要好处。事实上,这并不是在编写bug较少的软件,也不是在编写更好的软件,因为TDD不能保证两者都有(至少本质上没有)。TDD迫使你思考设计决策,并做出所有的设计决策。这个时间(并减少调试时间。)如果你在做TDD时感到痛苦,通常是因为设计决策会反噬你。然后是时候换上重构的帽子,改进设计了。

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

至于测试是否设置了属性。如果我理解正确的话,你只是为了测试而暴露了这些属性?如果那样的话,我建议不要那样做。假设你有一个带有构造函数参数的类,并且有一个测试断言构造函数应该抛出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会告诉你,这个方法/类做得太多了,可能希望被分成易于测试的部分。例如,一个用于验证的类、一个用于映射的类和一个用于执行存储调用的类。

不过,这很可能会导致过度工程化!所以要注意这一点,你会有一种感觉,知道什么时候该抵制更多间接的冲动。)

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

通常在单元测试中(尤其是TDD中的),您不会测试正在测试的类中的每一条语句。TDD单元测试的主要目的是测试类的业务逻辑,而不是初始化内容。

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

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

注意:还有其他类型的单元测试,例如代码覆盖率测试,用于测试类中的每一个代码路径。然而,我个人认为这些测试不切实际,当然TDD中也不鼓励这样做。