想出一个好的答案并不容易,不知道你在实现中所做的计算类型的任何细节,但我很佩服你愿意进行单元测试,所以无论如何我都会尽力而为,我希望答案不会太长。;)
0. 远大的期望?
老实说,可能没有一个与您的问题完全匹配的答案 - 做一件事的许多方法都可以正确地完成工作,而适用于单元测试的唯一基本规则是它们应该可靠地帮助您证明您的系统是稳定的。如果他们不这样做,你不应该费心去写它们。但是,如果这可以通过创建一个包含一百万行不同输入和输出值组合的 Excel 工作表,并以 CSV 格式将其提供给单元测试中的 for 循环来完成......
好的,也许有更好的方法。但最终,这一切都取决于你想做到这一点有多彻底,以及你愿意在多大程度上偏离你已经完成的工作以使你的测试更好。
1.为一些聪明的言论做好准备
从我在字里行间读到的内容来看,您并没有花很多时间考虑可测试性,因为您在编写测试之前就已经编写了代码。遗憾的是,这确实不是进行单元测试的最佳方式:您添加到生产代码中的每一行都应该在您编写它之前就被失败的单元测试覆盖。只有这样,您才能始终确保您的系统正常工作——而且它始终是可测试的!听起来很累?不会的,习惯了就好了。
我不会在基础方面给你太多麻烦,但如果你真的对单元测试很认真,我建议你开始将TDD应用到所有未来的项目中:要开始,也许可以在cleancoders.com上观看 TDD 剧集- Uncle Bob 在解释这些事情方面做得比我好得多,而且他看起来很有趣(虽然他的演示是用 Java 编写的,但这应该不是什么大问题 - TDD 的基本原则适用于所有语言)。
同时,我仍然会根据您的问题发表一些聪明的评论。告我 ;)
2. 聪明的评论#1:如何测试?
确保您记住测试的目标是证明您正在测试的代码正确工作,而不是针对每种可能的参数组合重复证明它。您应该始终将断言的数量保持在证明您的代码正确所需的最低限度。
那么,这将回答您的第一个问题:您应该只有一个测试来证明您正在测试的每个算法的正确性。 输入和输出值的不同组合可用于该测试中的断言。
为每个算法添加更多测试的唯一原因是当您测试失败时,即如果您null
作为参数传递或任何违反约束的东西会发生什么。每次在失败的情况下抛出错误时,都应该在单独的测试中进行测试。
然而,更复杂的是选择从哪个抽象级别开始编写测试。通常不需要为类中的每个方法编写测试,尤其是私有方法。这是应用 TDD 的另一个原因——它让您从外向内思考您正在尝试做什么,即您测试系统的一部分应该做什么,而不是测试每个实现细节。当您在编写代码之前进行测试时,当您注意到您的程序已经增长并且事情变得更加复杂时,很容易在这里和那里添加测试;“事后”总是更难做到这一点。
3. 聪明的评论#2:测试什么?
您的程序设计的目标应该是使您的单元尽可能与系统的其他部分分离。这意味着将事物组合应用到一个单元中的另一种事物组合可能不是好的设计。您应该能够仅测试在您正在测试的单元中实现的代码,与所有其他事物分开。这表示
4. 将其应用于您的问题的微弱尝试
我为什么要告诉你这一切?在我看来,您的方法更像是一个集成测试:有一个黑盒要测试,并且可以从中产生无数的东西。这在某些情况下是可以的,但您仍应尝试使该黑匣子尽可能小。
现在,由于我对您正在做的实际数学一无所知,因此我将从这里开始做一些假设。如果这些不合适,我很抱歉,但如果您提供一些代码示例,我很乐意添加或更改信息。
显而易见的第一个猜测:您根据Rectanglebar
的坐标值对数组的所有成员重复应用相同的计算。foo
这意味着您实际上在方法中做了两件事:a)遍历bar
数组和 b)应用公式:
public function foobar ( foo:Rectangle, bar:Array ) : void {
for each ( var rect:Rectangle in bar) {
// things done to rect based on foo
}
}
如果是这种情况,您可以轻松地改进您的架构。第一步是分离公式:
public function foobar ( foo:Rectangle, bar:Array ) : void {
for each ( var rect:Rectangle in bar) {
applyFooValuesToRect( foo, rect);
}
}
public function applyFooValuesToRect ( foo : Rectangle, rect : Rectangle ) : void {
// things done to rect based on foo
}
现在你会看到你真正应该测试的是applyFooValuesToRect
方法——它突然让你的测试变得更容易了。
我还可以想象迭代可能会有变化:一个实现适用foo
于所有bar
,一个匹配某些标准并且仅适用于正匹配,也许一个基于每个bar
值对 foo 进行一系列计算,一个可能使用两个公式而不是一个,等等。如果其中任何一个适用于您的项目,您可以通过使用策略模式大大改进您的 API,并降低复杂性。对于 40 种变体中的每一种,使实际公式成为实现公共Formula
接口的单独类:
public interface Formula {
function applyFooToBar (foo:Rectangle, bar:Rectangle) : Rectangle;
}
public class FormulaOneImpl implements Formula {
public function applyFooToBar (foo:Rectangle, bar:Rectangle) : Rectangle {
// do things to bar
return bar;
}
}
public class FormulaTwoImpl implements Formula ... // etc.
您现在可以分别测试每个公式,并将断言应用于返回的值。
您的原始类将采用 type 的字段变量Formula
:
public class MyGreatImpl implements OriginalInterface {
public var formula:Formula;
//..
public function foobar (foo:Rectangle, bar:Array):void {
for each (var rect:Rectangle in bar) formula.applyFooToBar (foo, rect);
}
}
然后,您可以传入各种公式——只要它们实现了接口。因此,您现在可以使用该接口创建模拟对象来测试算法的所有其他部分:Formula
模拟对象所要做的就是验证applyFooToBar
被调用的对象,并返回您为每个断言设置的预定值。例如,通过这种方式,您可以确保在测试数组的迭代时确实没有测试公式。
实际上,您也可以尝试将其应用于其他事物:ACriteriaMatcher
也可以构成一个不错的策略,首先...
当你像这样分解你的代码时,你可能会看到你有多个实现依赖于相同的公式,但迭代循环有不同的变体等。它甚至可能允许你减少实现的数量您的初始界面 - 因为现在将变体封装到不同的策略类中。
男孩,这是一个很长的文本。我现在停止咆哮。我希望我能在这方面对你有所帮助——如果我的假设离我太远的话,只需再次评论或编辑你的问题。也许我们可以进一步缩小可能的解决方案。