testing - 此 TDD 尝试的最佳实践

标签 testing tdd

我编码了大约 12 年,但一直以来我从未习惯 TDD。

嗯,事情即将改变,但由于我是自学,所以我希望你们能帮助我。

我正在发布一个非常简单的胸部类别的游戏示例。 当玩家捕获宝箱时,它会记录当前获得宝箱的时间。 这个箱子需要一些时间才能打开,因此,出于用户界面的原因,我需要显示打开所需的剩余时间。 每个箱子都有一个类型,该类型与打开所需时间的数据库值绑定(bind)。

这是一种“不测试,只是快速完成事情”的心态。考虑 ChestsDatabase 和 DateManager 是包含数据库绑定(bind)值和包装在类中的当前系统时间的单例。

public class Chest {
    private readonly int _type;
    private readonly float _timeObtained;

    public Chest(int type, float timeObtained) {
        _type = type;
        _timeObtained = timeObtained;
    }

    public bool IsOpened() {
        return GetRemainingTime() <= 0;
    }

    // It depends heavily on this concrete Singleton class
    public float GetRemainingTime() {
        return ChestsDatabase.Instance.GetTimeToOpen(_type) - GetPassedTime();
    }

    // It depends heavily on this concrete Singleton class
    private float GetPassedTime() {
        return DateManager.Instance.GetCurrentTime() - _timeObtained;
    }
}

当然,我可以以依赖注入(inject)的方式做到这一点并摆脱单例:

public class Chest {
    private readonly ChestsDatabase _chestsDatabase;
    private readonly DateManager _dateManager;
    private readonly int _type;
    private readonly float _timeObtained;

    public Chest(ChestsDatabase chestsDatabase, DateManager dateManager, int type, float timeObtained) {
        _chestsDatabase = chestsDatabase;
        _dateManager = dateManager;
        _type = type;
        _timeObtained = timeObtained;
    }

    public bool IsOpened() {
        return GetRemainingTime() <= 0;
    }

    public float GetRemainingTime() {
        return _chestsDatabase.GetTimeToOpen(_type) - GetPassedTime();
    }

    private float GetPassedTime() {
        return _dateManager.GetCurrentTime() - _timeObtained;
    }
}

如果我使用接口(interface)来表达相同的逻辑怎么办?这将更加“TDD 友好”,对吧? (当然,假设我已经完成了测试)

public class Chest {
    private readonly IChestsDatabase _chestsDatabase;
    private readonly IDateManager _dateManager;
    private readonly int _type;
    private readonly float _timeObtained;

    public Chest(IChestsDatabase chestsDatabase, IDateManager dateManager, int type, float timeObtained) {
        _chestsDatabase = chestsDatabase;
        _dateManager = dateManager;
        _type = type;
        _timeObtained = timeObtained;
    }

    public bool IsOpened() {
        return GetRemainingTime() <= 0;
    }

    public float GetRemainingTime() {
        return _chestsDatabase.GetTimeToOpen(_type) - GetPassedTime();
    }

    private float GetPassedTime() {
        return _dateManager.GetCurrentTime() - _timeObtained;
    }
}

但是我到底该怎么测试这样的东西呢? 会是这样吗?

    [Test]
    public void SomeTimeHavePassedAndReturnsRightValue()
    {
        var mockDatabase = new MockChestDatabase();
        mockDatabase.ForType(0, 5); // if Type is 0, then takes 5 seconds to open
        var mockManager = new MockDateManager();
        var chest = new Chest(mockDatabase, mockManager, 0, 6); // Got a type 0 chest at second 6
        mockManager.SetCurrentTime(8); // Now it is second 8
        Assert.AreEqual(3, chest.GetRemainingTime()); // Got the chest at second 6, now it is second 8, so it passed 2 seconds. We need 5 seconds to open this chest, so the remainingTime is 3
    }

这在逻辑上正确吗?我错过了什么吗?因为这看起来太大了,太复杂了,太……错误了。为了这些测试的目的,我不得不创建 2 个额外的类~只是~。

让我们看看模拟框架:

    [Test]
    public void SomeTimeHavePassedAndReturnsRightValue()
    {
        var mockDatabase = Substitute.For<IChestsDatabase>();
        mockDatabase.GetTimeToOpen(0).Returns(5);
        var mockManager = Substitute.For<IDateManager>();
        var chest = new Chest(mockDatabase, mockManager, 0, 6);
        mockManager.GetCurrentTime().Returns(8);
        Assert.AreEqual(3, chest.GetRemainingTime());
    }

我用框架删除了两个类,但我仍然觉得有问题。我的逻辑有更简单的方法吗?在这种情况下,您会使用模拟框架还是实现的类?

你们会完全放弃测试还是坚持我的任何解决方案?或者如何使这个解决方案变得更好?

希望您能在我的 TDD 之旅中为我提供帮助。谢谢。

最佳答案

对于您当前的设计,您的最后一次尝试在逻辑上是正确的,并且接近我认为的最佳测试用例。

我建议将模拟变量提取到字段中。我还会重新排序测试线,以明确区分设置、执行和验证。将胸部类型提取为常量也使测试更容易理解。

private IChestsDatabase  mockDatabase = Substitute.For<IChestsDatabase>();
private IDateManager mockManager = Substitute.For<IDateManager>();
private const int DefaultChestType = 0;

[Test]
public void RemainingTimeIsTimeToOpenMinusTimeAlreadyPassed()
{
    mockDatabase.GetTimeToOpen(DefaultChestType).Returns(5);
    mockManager.GetCurrentTime().Returns(6+2);
    var chest = new Chest(mockDatabase, mockManager, DefaultChestType, 6);

    var remainingTime = chest.GetRemainingTime();

    Assert.AreEqual(5-2, remainingTime);
}

现在进行更一般性的评论。 TDD 的主要好处是它可以为您提供有关设计的反馈。您对测试代码庞大、复杂且错误的感觉是一个重要的反馈。将其视为 design pressure 。测试将通过测试重构以及设计改进而得到改善。

对于您的代码,我会考虑以下设计问题:

  1. 职责分配是否正确?特别是,Chest 有责任了解已经过去的时间和剩余时间吗?
  2. 设计中是否缺少任何概念?也许每个箱子都有一个锁,并且有一个时基锁。
  3. 如果我们在构造时将 TimeToOpen 而不是 Type 传递给 Chest 会怎么样?可以把它想象成传递一根针,而不是传递大海捞针,因为大海捞针还没有被发现。如需引用,请参阅this post

有关测试如何提供设计反馈的详细讨论,请参阅 Steve Freeman 和 Nat Pryce 编写的《Growing Object Oriented Software Guided by Tests》。

对于用 C# 编写可读测试的一套良好实践,我推荐 Roy Osherove 的《单元测试的艺术》。

关于testing - 此 TDD 尝试的最佳实践,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43603732/

相关文章:

javascript - JavaScript Web 前端的测试驱动开发

ruby-on-rails - Rspec 中预期错误和其他状态

Angular CLI ng e2e : Error: connect ETIMEDOUT

javascript - 带有业力的覆盖率报告以及 javascript 和 typescript src 文件的混合

testing - 其他几个运行者在 testcafe 中的运行大师?

ruby-on-rails - 如果没有 any_instance,如何断言没有进行方法调用?

spring - 如何将来自规则的值注入(inject)测试 Spring 上下文?

javascript - React/Flux + Jasmine - 预期 spy 被调用失败

unit-testing - 开发 DBA 的 TDD 方法?

c++ - 你如何运行你的单元测试?编译器标志?静态库?