僵尸不擅长理解边界。他们践踏栅栏,推倒墙壁,并且通常进入他们不应该进入的地方。在本系列的前几篇文章中,我解释了为什么一次性解决编码问题,就像面对成群僵尸一样,是一个错误。
ZOMBIES 是一个首字母缩写,代表
Z – 零
O – 一
M – 多 (或更复杂)
B – 边界行为
I – 接口定义
E – 练习异常行为
S – 简单场景,简单解决方案
在本系列的前两篇文章中,我演示了 ZOMBIES 原则的前三个:零、一 和 多。第一篇文章 实现了 Zero,它提供了通过代码的最简单路径。第二篇文章 使用 One 和 Many 样本进行了测试。在第三篇文章中,我将看看 Boundaries 和 Interfaces.
回到一
在你可以处理 Boundaries 之前,你需要回到原点 (迭代)。
首先问问自己:电子商务中的边界是什么?我需要或想要限制购物篮的大小吗?(实际上,我认为这没有任何意义)。
此时唯一合理的边界是确保购物车永远不包含负数的商品。编写一个可执行的期望来表达这个限制
[Fact]
public void Add1ItemRemoveItemRemoveAgainHas0Items() {
var expectedNoOfItems = 0;
var actualNoOfItems = -1;
Assert.Equal(expectedNoOfItems, actualNoOfItems);
}
这意味着,如果你向购物车添加一件商品,删除该商品,然后再次删除,shoppingAPI
实例应该显示你的购物车中有零件商品。
当然,正如预期的那样,这个可执行的期望(微测试)失败了。你需要进行的最少修改是什么才能使这个微测试通过?
[Fact]
public void Add1ItemRemoveItemRemoveAgainHas0Items() {
var expectedNoOfItems = 0;
Hashtable item = new Hashtable();
shoppingAPI.AddItem(item);
shoppingAPI.RemoveItem(item);
var actualNoOfItems = shoppingAPI.RemoveItem(item);
Assert.Equal(expectedNoOfItems, actualNoOfItems);
}
这编码了一个依赖于 RemoveItem(item)
功能的期望。并且由于你的 shippingAPI
中没有该功能,你需要添加它。
转到 app
文件夹,打开 IShippingAPI.cs
并添加新的声明
int RemoveItem(Hashtable item);
转到实现类 (ShippingAPI.cs
),并实现声明的功能
public int RemoveItem(Hashtable item) {
basket.RemoveAt(basket.IndexOf(item));
return basket.Count;
}
运行系统,你会得到一个错误

(Alex Bunardzic, CC BY-SA 4.0)
系统正尝试删除购物车中不存在的商品,并且崩溃了。添加一点防御性编程
public int RemoveItem(Hashtable item) {
if(basket.IndexOf(item) >= 0) {
basket.RemoveAt(basket.IndexOf(item));
}
return basket.Count;
}
在尝试从购物车中删除商品之前,请检查它是否在购物车中。(你可以尝试捕获异常,但我认为上面的逻辑更容易阅读和理解。)
更具体的期望
在我们转向更具体的期望之前,让我们暂停一下,检查一下接口的含义。在软件工程中,接口表示规范,或对某些功能的描述。在某种程度上,软件中的接口类似于烹饪中的食谱。它列出了制作蛋糕的成分,但实际上不可食用。我们按照食谱中指定的描述来烘烤蛋糕。
同样,在这里,我们首先指定此服务能够做什么来定义我们的服务。该规范就是我们所说的接口。但是接口本身无法为我们提供任何服务。它只是一个蓝图,我们然后使用并遵循该蓝图来实现指定的功能。
到目前为止,你已经实现了接口(部分;稍后将添加更多功能)和处理边界(购物车中不能有负数的商品)。你指示了 shoppingAPI
如何将商品添加到购物车,并通过运行 Add2ItemsBasketHas2Items
测试确认了添加功能有效。
然而,仅仅将商品添加到购物车并不能构成一个电子商务应用程序。你需要能够计算添加到购物车的商品总额——是时候添加另一个期望了。
按照现在的惯例(希望如此),从最直接的期望开始。当你在购物车中添加一件商品且商品价格为 10 美元时,你希望购物 API 正确计算总额为 10 美元。
你的第五个测试(虚假版本)
[Fact]
public void Add1ItemPrice10GrandTotal10() {
var expectedTotal = 10.00;
var actualTotal = 0.00;
Assert.Equal(expectedTotal, actualTotal);
}
通过使用古老的技巧:硬编码一个不正确的实际值,使 Add1ItemPrice10GrandTotal10
测试失败。当然,你之前的三个测试成功了,但是新的第四个测试失败了
A total of 1 test files matched the specified pattern.
[xUnit.net 00:00:00.57] tests.UnitTest1.Add1ItemPrice10GrandTotal10 [FAIL]
X tests.UnitTest1.Add1ItemPrice10GrandTotal10 [4ms]
Error Message:
Assert.Equal() Failure
Expected: 10
Actual: 0
Test Run Failed.
Total tests: 4
Passed: 3
Failed: 1
Total time: 1.0320 Seconds
用实际处理替换硬编码的值。首先,看看你的接口中是否有任何可以计算订单总额的功能。没有,没有这样的功能。到目前为止,你在接口中只声明了三个功能
int NoOfItems();
int AddItem(Hashtable item);
int RemoveItem(Hashtable item);
这些功能都没有表明任何计算总额的能力。你需要声明一个新功能
double CalculateGrandTotal();
这个新功能应该使你的 shoppingAPI
能够通过遍历在购物车中找到的商品集合并加总商品价格来计算总金额。
转到你的测试并更改第五个测试
[Fact]
public void Add1ItemPrice10GrandTotal10() {
var expectedGrandTotal = 10.00;
Hashtable item = new Hashtable();
item.Add("00000001", 10.00);
shoppingAPI.AddItem(item);
var actualGrandTotal = shoppingAPI.CalculateGrandTotal();
Assert.Equal(expectedGrandTotal, actualGrandTotal);
}
此测试声明了你的期望,即如果你添加一件价格为 10 美元的商品,然后在购物 API 上调用 CalculateGrandTotal()
方法,它将返回 10 美元的总额。这是一个完全合理的期望,因为 API 应该这样计算。
你如何实现此功能?像往常一样,首先伪造它。转到 ShippingAPI
类并实现 CalculateGrandTotal()
方法,如接口中声明的那样
public double CalculateGrandTotal() {
return 0.00;
}
你将返回值硬编码为 0.00,只是为了看看测试(你的第一个客户)是否能够运行它以及它是否会失败。事实上,它确实运行良好并且失败了,所以现在你必须实现处理逻辑来正确计算购物车中商品的总额
public double CalculateGrandTotal() {
double grandTotal = 0.00;
foreach(var product in basket) {
Hashtable item = product as Hashtable;
foreach(var value in item.Values) {
grandTotal += Double.Parse(value.ToString());
}
}
return grandTotal;
}
运行系统。所有五个测试都成功了!
从一到多
是时候进行另一次迭代了。现在你已经通过迭代构建了系统来处理 Zero、One(包括非常简单的和稍微复杂的场景)和 Boundary 场景(购物车中没有负数商品),你必须处理 Many 的稍微复杂的场景。
快速说明:当我们不断迭代并返回到与 One、Many 和 Boundaries 相关的问题时(我们正在改进我们的实现),一些读者可能会期望我们也应该重做 Interface. 正如我们稍后将看到的,我们的接口已经完全成熟,我们认为此时无需添加更多功能。请记住,接口应保持精简和简单;增殖接口没有太多优势,因为这只会给信号增加更多噪声。在这里,我们遵循奥卡姆剃刀原则,该原则指出,如果没有充分的理由,实体不应倍增。目前,我们已经基本完成了 API 预期功能的描述。我们现在正在卷起袖子并改进实现。
之前的迭代使系统能够处理放置在购物车中的多个商品。现在,使系统能够计算购物车中多个商品的总额。首先;编写可执行的期望
[Fact]
public void Add2ItemsGrandTotal30() {
var expectedGrandTotal = 30.00;
var actualGrandTotal = 0.00;
Assert.Equal(expectedGrandTotal, actualGrandTotal);
}
你通过首先硬编码所有值来“作弊”,然后尽最大努力确保期望失败。
它确实失败了,所以现在是时候让它通过了。通过向购物车添加两件商品,然后运行 CalculateGrandTotal()
方法来修改你的期望
[Fact]
public void Add2ItemsGrandTotal30() {
var expectedGrandTotal = 30.00;
Hashtable item = new Hashtable();
item.Add("00000001", 10.00);
shoppingAPI.AddItem(item);
Hashtable item2 = new Hashtable();
item2.Add("00000002", 20.00);
shoppingAPI.AddItem(item2);
var actualGrandTotal = shoppingAPI.CalculateGrandTotal();
Assert.Equal(expectedGrandTotal, actualGrandTotal);
}
它通过了。你现在有六个微测试成功通过;系统恢复到稳定状态!
设置期望
作为一名认真的工程师,你想确保当用户向购物车添加商品然后从购物车中删除一些商品时,预期的复杂操作始终计算出正确的总额。这是新的期望
[Fact]
public void Add2ItemsRemoveFirstItemGrandTotal200() {
var expectedGrandTotal = 200.00;
var actualGrandTotal = 0.00;
Assert.Equal(expectedGrandTotal, actualGrandTotal);
}
这意味着当有人向购物车添加两件商品,然后删除第一件商品时,预期的总额为 200.00 美元。硬编码的行为失败了,现在你可以用更具体的确认示例和运行代码来详细说明
[Fact]
public void Add2ItemsRemoveFirstItemGrandTotal200() {
var expectedGrandTotal = 200.00;
Hashtable item = new Hashtable();
item.Add("00000001", 100.00);
shoppingAPI.AddItem(item);
Hashtable item2 = new Hashtable();
item2.Add("00000002", 200.00);
shoppingAPI.AddItem(item2);
shoppingAPI.RemoveItem(item);
var actualGrandTotal = shoppingAPI.CalculateGrandTotal();
Assert.Equal(expectedGrandTotal, actualGrandTotal);
}
你的确认示例,编码为期望,添加第一件商品(ID “00000001”,商品价格 100.00 美元),然后添加第二件商品(ID “00000002”,商品价格 200.00 美元)。然后,你从购物车中删除第一件商品,计算总额,并断言它是否等于期望值。
当这个可执行的期望运行时,系统通过正确计算总额来满足期望。你现在有七个测试通过了!系统正在工作;没有任何问题!
Test Run Successful.
Total tests: 7
Passed: 7
Total time: 0.9544 Seconds
更多内容即将到来
你现在已经到了 ZOMBI,所以在下一篇文章中,我将介绍 E。在那之前,尝试一下你自己的测试!
评论已关闭。