2017-04-25 73 views
2

我编码了大约12年,但我从来没有习惯过TDD。此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 
    } 

这是合乎逻辑的吗?我错过了什么吗?因为这看起来很大,如此错综复杂,所以......错了。为了进行这些测试,我必须创建两个额外的课程。

让我们带着嘲弄的框架看:

[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旅程中帮助我。谢谢。

回答

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到胸部怎么办?把它看作是通过针头,而不是通过干草堆,其中针还没有找到。仅供参考,请参阅this post

为了测试如何能提供设计反馈商量好了,指的是由史蒂夫·弗里曼和NAT普赖斯测试的指导下成长的面向对象的软件。

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

2

但是也有一些需要而编写单元测试,如图单元测试

  1. 单独的项目要考虑的一些要点。

  2. 一类用于编写一类主要 代码中函数的单元测试。

  3. 功能内涵盖条件。
  4. 测试驱动开发(TDD)

如果你真的想知道更多(举例),看看这个教程

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