此TDD尝试的最佳做法

时间:2017-04-25 06:59:15

标签: testing tdd

我编码了大约12年,但我从来没有习惯过TDD。

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

我正在为一个非常简单的胸部课程发布游戏示例。 当玩家抓住胸部时,它会记录获得的当前时间。 这个胸部需要一些时间才能打开,因此,出于UI的原因,我需要显示打开所需的剩余时间。 每个箱子都有一个类型,这种类型与数据库值绑定在一起需要多长时间才能打开。

这是一种“无需测试 - 只是让事情做得更快 - 快速思维”。考虑到ChestsDatabase和DateManager是包含数据库绑定值和当前系统时间的单例。

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;
    }
}

当然,我本可以以依赖注入方式制作它并摆脱单身人士:

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;
    }
}

如果我使用接口来表达相同的逻辑怎么办?这将更加“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之旅中帮助我。感谢。

2 个答案:

答案 0 :(得分:3)

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

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

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. 责任是否正确分配?特别是,知道通过和剩余时间是否真的是胸部的责任?
  2. 设计中是否缺少任何概念?也许每个箱子都有一个锁,还有一个时基锁。
  3. 如果我们在施工时通过TimeToOpen而不是Type to Chest怎么办?可以把它想象成通过针而不是通过大海捞针,其中针还没有找到。有关参考,请参阅this post
  4. 有关测试如何提供设计反馈的详细讨论,请参阅Steve Freeman和Nat Pryce的测试指导下的面向对象的增长软件。

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

答案 1 :(得分:2)

在编写单元测试时需要考虑一些要点

  1. 单独测试项目。

  2. 一类用于在一类main中编写函数的单元测试 代码

  3. 涵盖职能范围内的条件。
  4. 测试驱动开发(TDD)
  5. 如果您真的想了解更多(使用示例),请查看本教程

    单元测试c# - 最佳实践https://www.youtube.com/watch?v=grf4L3AKSrs