3

我一直潜伏在这里寻求帮助,现在我找不到我当前问题的答案。

返回信息

我正在写一些单元测试(耶!)。我有 40 个实现接口的对象。该接口中的一个函数接受两个参数,一个矩形和一个矩形数组:

public function foobar(foo:Rectangle, bar:Array/*Rectangle*/):void;

我想为这 40 个对象中的每一个编写测试。为了确保我测试所有可能性,我需要在 foo 和 bar 的变化(长度和内容)有变化的地方运行测试。所以 foo 的 x 个数和 foo 中 1 到 x 个 Rectangle 数。

每个实现该接口的对象都在运行一个算法,该算法将对 bar 中的每个 Rectangle 进行一些计算并更改它们的属性。每种算法都会产生截然不同的结果。

如果我选择有 10 个可能的 foo 对象和 10 个可能的 bar 数组对象,我最终会写数千个!的测试。我不想手写数千个测试。


问题

编写一个算法来获取可能的对象,并对产生结果的所有可能配置进行runts测试,然后我回去手动检查所有结果是否正确,对我来说是否太落后了?这只是进行单元测试的错误方法吗?

运行产生结果的算法是否错误,然后手动检查输出?

我的另一个想法是我为算法提供可能的对象,它会吐出一些为测试工具格式化的 xml 或 json,然后我通过每个测试,填写缺少的断言值,然后将它们提供给他们?

我的另一个计划是编写一个算法,该算法接受 foo Rectangle 列表和要在 bar 中使用的可能 Rectangle 列表,并让该算法以适用于我的测试工具的格式生成 JSON(它包括断言)。由于生成 JSON 的算法不知道断言,因此我会在通过测试工具发送之前将其手写。

这是一种常见的做法吗?


感谢您的任何反馈:)

4

1 回答 1

2

想出一个好的答案并不容易,不知道你在实现中所做的计算类型的任何细节,但我很佩服你愿意进行单元测试,所以无论如何我都会尽力而为,我希望答案不会太长。;)

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也可以构成一个不错的策略,首先...

当你像这样分解你的代码时,你可能会看到你有多个实现依赖于相同的公式,但迭代循环有不同的变体等。它甚至可能允许你减少实现的数量您的初始界面 - 因为现在将变体封装到不同的策略类中。

男孩,这是一个很长的文本。我现在停止咆哮。我希望我能在这方面对你有所帮助——如果我的假设离我太远的话,只需再次评论或编辑你的问题。也许我们可以进一步缩小可能的解决方案。

于 2012-03-10T15:31:21.660 回答