如何使用复杂的编排逻辑设计和测试方法?

时间:2014-08-04 20:17:17

标签: unit-testing testing dry

我将用简短的例子开始我的问题:

SomeResult DoSomething(input)
{
    var a = svc1.getA(input);
    if (condition with a)
    {
        var b = svc2.getB(a);
        if (cond with b)
        {
            var c = svc3.getC(b);
            if (cond with c)
            {
            }
            else
            {
            }
        }
        else
        {
        }
    }
    else
    {
    }
}

我相信这个想法很清楚。我们有复杂的分支逻辑,其条件取决于注入服务返回的中间结果。

当我们想要cond with c的部分时,我们必须模仿svc1svc2以及svc3。 要在cond with b处投放,我们必须模仿svc1svc2

因此,每当我们进一步深入时,我们都会重放执行路径的所有上部。猜猜通常是怎么做的?宾果,复制粘贴!
我们有一系列单元测试,其中大部分线路被物体占据。 (a,b,c ...)初始化和服务模拟。当abc是具有数十个属性的对象时,所有这些看起来都是真正的地狱。 cond with a的微小变化可以轻易地同时打破20个测试。

我坚持要有一些关于跳跃海峡到我想测试的地方的概念"。

如果我们改变代码怎么办:

SomeResult DoSomething(input)
{
    var a = svc1.getA(input);
    if (condition with a)
    {
        var b = svc2.getB(a);
        if (cond with b)
        {
            ProcessBLikeThis(b);
        }
        else
        {
        }
    }
    else
    {
    }
}

然后我们可以将ProcessBLikeThis与不相关的逻辑分开测试 然而,要使它成为可测试的,它必须是公开的。此外,由于我们希望测试验证ProcessBLikeThis是否根据cond with b使用给定参数调用,我们需要使用隔离器或使ProcessBLikeThis成为某种接口的方法。 / p>

然而,除了DRY粘附可测试性之外,没有其他必要的粒度设计。

所以我非常感谢在这里指导如何设计和测试这些方法。

增加:

我还忘了提到我的队友强烈反对将初始化逻辑放在可重用的方法中,因为他们看不到可以放在那里的东西和什么不可以的东西之间没有严格的边界线,并期望有一天有人会扩展代码并打破测试逻辑。他们更喜欢复制粘贴作为隔离的手段。

1 个答案:

答案 0 :(得分:2)

  

我的队友强烈反对将初始化逻辑放在可重用的方法中,因为他们发现可以放在那里的东西和不能放在那里的东西之间没有严格的界限,并期望有一天有人会扩展代码并破坏测试逻辑。他们更喜欢复制/粘贴作为隔离的手段。

如果你的团队想要重复自己并且你不想要他们,那么讨论需要进行并形成共识,以便每个人都在考虑相同的目标。有一些参数可以重复一些测试设置代码,通常是可读性,但是,这通常可以通过合理地命名任何方法和变量来克服,以便它们的使用是显而易见的。

测试无法重用代码的论点,因为有人可能会更改共享代码并中断测试,这是一个空参数。如果有人做了更改了共享逻辑并且一堆测试没有破坏你会遇到更大的问题。正如您所说,更可能的情况是生产代码中的一个小变化将导致一堆测试失败。如果测试没有共享相关的设置代码,那么 fix 可能会盲目地复制/粘贴到每个测试中以使它们工作。

也就是说,简化测试的常用方法是创建不同级别的间接,这样您就可以减少测试。使用您发布的代码,一种方法可能是将流逻辑与操作逻辑分开。

你可能会得到类似这样的代码(这些名称显然需要根据你的情况量身定制):

interface ISomeActioner {
    bool IsTriggered( SomeStateProvider state);
    SomeResult TriggeredAction(SomeStateProvider state);
    SomeResult UntriggeredAction(SomeStateProvider state);
}

SomeResult DoSomething(input) {
    SomeResult result = Unknown;
    foreach(var actioner in _someActions) {
        if(IsTriggered(/* some state provider */)) {
            result = actioner.TriggeredAction(/* some state provider */);
        } else {
            result = actioner.UntriggeredAction(/* some state provider */);
        }
        if(result != Unknown) break;
    }
    return result;
}

然后,您最终实现了几个实现ISomeActioner接口的类。这些课程中的每一课都很简单;它从状态提供程序检查状态并返回一个标志,指示应该调用哪个其他函数。可以单独测试这些类,以确保每个公共方法都能完成预期的操作,方法是在调用每个方法之前将SomeStateProvider设置为适当的状态。

然后需要将这些类的有序列表注入包含DoSomething方法的类中。这允许您在测试DomeSomething方法时使用接口的模拟实例,这有效地测试了for循环。