此 TDD 尝试的最佳实践
Best Practices for this TDD attempt
我编码大约 12 年,但我一直没有习惯 TDD。
好吧,事情即将发生变化,但由于我是自学的,所以我希望你们能帮助我。
我正在发布一个非常简单的宝箱的游戏示例 class。
当玩家抓取箱子时,它会记录当前获得箱子的时间。
这个箱子需要一些时间才能打开,因此,出于 UI 的原因,我需要显示打开所需的剩余时间。
每个箱子都有一个类型,这个类型绑定到一个数据库值,表示打开需要多长时间。
这是一个"no-testing-just-get-things-done-fast-mindset"。考虑 ChestsDatabase 和 DateManager 是包含数据库绑定值和包装在 class.
中的当前系统时间的单例
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-friendly",对吧? (当然假设我已经先完成了测试)
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 个 classes ~just~。
让我们看看模拟框架:
[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());
}
我用框架去掉了两个 classes,但我仍然觉得有问题。我的逻辑中有更简单的方法吗?在这种情况下,您会使用模拟框架还是实施 classes?
你们会完全放弃测试还是会坚持我的任何解决方案?或者如何使这个解决方案更好?
希望您能在我的 TDD 之旅中为我提供帮助。谢谢。
如图所示,在编写单元测试时需要考虑一些要点
单元测试的单独项目。
一个 class 用于在一个 class 的 main 中编写函数的单元测试
代码.
- 覆盖函数内的条件。
- 测试驱动开发 (TDD)
如果你真的想了解更多(有例子),看看这个教程
C# 单元测试 - 最佳实践https://www.youtube.com/watch?v=grf4L3AKSrs
对于您当前的设计,您最后一次尝试在逻辑上是正确的,并且接近我认为的最佳测试用例。
我建议将模拟变量提取到字段中。我还会重新排序测试线,以明确区分设置、执行和验证。将胸型提取为常量也让测试更容易理解。
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。测试将通过测试重构以及设计改进来改进。
对于你的代码,我会考虑这些设计问题:
- 职责分配是否合理?特别是,Chest 有责任知道过去和剩余的时间吗?
- 设计中是否缺少任何概念?可能每个宝箱都有一个Lock,还有一个时基Lock。
- 如果我们在构造时将 TimeToOpen 而不是 Type 传递给 Chest 会怎样?把它想象成穿过一根针而不是穿过大海捞针,在大海捞针还没有找到。有关参考,请参阅 this post
有关测试如何提供设计反馈的详细讨论,请参阅由 Steve Freeman 和 Nat Pryce 编写的由测试指导的成长中的面向对象软件。
关于用 C# 编写可读测试的一套良好实践,我推荐 Roy Osherove 的单元测试艺术。
我编码大约 12 年,但我一直没有习惯 TDD。
好吧,事情即将发生变化,但由于我是自学的,所以我希望你们能帮助我。
我正在发布一个非常简单的宝箱的游戏示例 class。 当玩家抓取箱子时,它会记录当前获得箱子的时间。 这个箱子需要一些时间才能打开,因此,出于 UI 的原因,我需要显示打开所需的剩余时间。 每个箱子都有一个类型,这个类型绑定到一个数据库值,表示打开需要多长时间。
这是一个"no-testing-just-get-things-done-fast-mindset"。考虑 ChestsDatabase 和 DateManager 是包含数据库绑定值和包装在 class.
中的当前系统时间的单例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-friendly",对吧? (当然假设我已经先完成了测试)
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 个 classes ~just~。
让我们看看模拟框架:
[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());
}
我用框架去掉了两个 classes,但我仍然觉得有问题。我的逻辑中有更简单的方法吗?在这种情况下,您会使用模拟框架还是实施 classes?
你们会完全放弃测试还是会坚持我的任何解决方案?或者如何使这个解决方案更好?
希望您能在我的 TDD 之旅中为我提供帮助。谢谢。
如图所示,在编写单元测试时需要考虑一些要点
单元测试的单独项目。
一个 class 用于在一个 class 的 main 中编写函数的单元测试 代码.
- 覆盖函数内的条件。
- 测试驱动开发 (TDD)
如果你真的想了解更多(有例子),看看这个教程
C# 单元测试 - 最佳实践https://www.youtube.com/watch?v=grf4L3AKSrs
对于您当前的设计,您最后一次尝试在逻辑上是正确的,并且接近我认为的最佳测试用例。
我建议将模拟变量提取到字段中。我还会重新排序测试线,以明确区分设置、执行和验证。将胸型提取为常量也让测试更容易理解。
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。测试将通过测试重构以及设计改进来改进。
对于你的代码,我会考虑这些设计问题:
- 职责分配是否合理?特别是,Chest 有责任知道过去和剩余的时间吗?
- 设计中是否缺少任何概念?可能每个宝箱都有一个Lock,还有一个时基Lock。
- 如果我们在构造时将 TimeToOpen 而不是 Type 传递给 Chest 会怎样?把它想象成穿过一根针而不是穿过大海捞针,在大海捞针还没有找到。有关参考,请参阅 this post
有关测试如何提供设计反馈的详细讨论,请参阅由 Steve Freeman 和 Nat Pryce 编写的由测试指导的成长中的面向对象软件。
关于用 C# 编写可读测试的一套良好实践,我推荐 Roy Osherove 的单元测试艺术。