如何简化Java中无副作用的方法的测试?

时间:2010-08-09 07:08:30

标签: java unit-testing junit functional-testing

函数(无副作用的函数)是一个基本的构建块,但我不知道用Java测试它们的令人满意的方法。

我正在寻找能够更轻松地测试它们的技巧。这是我想要的一个例子:

public void setUp() {
   myObj = new MyObject(...);
}

// This is sooo 2009 and not what I want to write:
public void testThatSomeInputGivesExpectedOutput () {
   assertEquals(expectedOutput, myObj.myFunction(someInput);
   assertEquals(expectedOtherOutput, myObj.myFunction(someOtherInput);
   // I don't want to repeat/write the following checks to see
   // that myFunction is behaving functionally.
   assertEquals(expectedOutput, myObj.myFunction(someInput);
   assertEquals(expectedOtherOutput, myObj.myFunction(someOtherInput);

}


// The following two tests are more in spirit of what I'd like 
// to write, but they don't test that myFunction is functional:
public void testThatSomeInputGivesExpectedOutput () {
   assertEquals(expectedOutput, myObj.myFunction(someInput);
}

public void testThatSomeOtherInputGivesExpectedOutput () {
   assertEquals(expectedOtherOutput, myObj.myFunction(someOtherInput);
}

我正在寻找一些可以放在测试,MyObject或myFunction上的注释,以使测试框架自动重复对myFunction的调用,对于我给出的给定输入/输出组合的所有可能的排列,或者可能的排列的一些子集,以证明该函数是有效的。

例如,在(仅)两个可能的排列之上是:

  • myObj = new MyObject();
  • myObj.myFunction(someInput);
  • myObj.myFunction(someOtherInput);

  • myObj = new MyObject();
  • myObj.myFunction(someOtherInput);
  • myObj.myFunction(someInput);

我应该只能提供输入/输出对(someInput,expectedOutput)和(someOtherInput,someOtherOutput),框架应该完成剩下的工作。

我没有使用过QuickCheck,但它看起来像是一个非解决方案。它被记录为发电机。我不是在寻找一种方法来为我的函数生成输入,而是一个框架,它允许我声明性地指定我的对象的哪个部分是无副作用的,并使用基于该声明的一些排列来调用我的输入/输出规范。 / p>

更新:我不打算验证对象没有任何变化,memoizing函数是这种测试的典型用例,而memoizer实际上改变了它的内部状态。但是,给定输入的输出始终保持不变。

8 个答案:

答案 0 :(得分:7)

如果你试图测试函数 没有副作用,那么用随机参数调用并不会真正削减它。这同样适用于具有已知参数的随机调用序列。或伪随机,随机或固定种子。很有可能只有在随机发生器选择的任何调用序列中才会发生(有害的)副作用。

无论输入是什么,也有可能在您正在进行的任何呼叫的输出中实际上看不到副作用。它们的副作用可能出现在你认为没有考虑过的其他相关对象上。

如果你想测试这种东西,你真的需要实现一个“白盒”测试,在那里你看一下代码并试着弄清楚可能导致(不需要的)副作用的原因并创建基于测试用例的测试用例在那些知识上。但我认为更好的方法是仔细的手动代码检查,或使用自动静态代码分析器...如果你能找到一个能为你完成工作的那个。

OTOH,如果你已经知道这些函数是无副作用的,那么实施随机测试“以防万一”有点浪费时间,IMO。

答案 1 :(得分:3)

我不太清楚我明白你在问什么,但似乎Junit Theories(http://junit.sourceforge.net/doc/ReleaseNotes4.4.html#theories)可能是一个答案。

答案 2 :(得分:1)

在此示例中,您可以创建键/值对的映射(输入/输出),并使用从地图中选取的值多次调用测试中的方法。这不能证明该方法是有效的,但会增加概率 - 这可能就足够了。

以下是这种额外可能功能测试的快速示例:

@Test public probablyFunctionalTestForMethodX() {
   Map<Object, Object> inputOutputMap = initMap(); // this loads the input/output values
   for (int i = 0; i < maxIterations; i++) {
     Map.Entry test = pickAtRandom(inputOutputMap); // this picks a map enty randomly
     assertEquals(test.getValue(), myObj.myFunction(test.getKey());
   }
}

基于命令模式可以解决更高复杂性的问题:您可以将测试方法包装在命令对象中,将命令对象添加到列表中,随机播放列表并根据命令执行命令(=嵌入式测试)那份清单。

答案 3 :(得分:0)

听起来您正在尝试测试在类上调用特定方法不会修改其任何字段。这是一个有点奇怪的测试用例,但完全有可能为它编写一个明确的测试。对于其他“副作用”,比如调用其他外部方法,它有点难度。您可以使用测试存根替换本地引用并验证它们是否未被调用,但您仍然不会以这种方式捕获静态方法调用。尽管如此,通过检查验证你在代码中没有做过类似的事情是有意义的,有时候必须足够好。

这是一种测试呼叫中没有副作用的方法:

public void test_MyFunction_hasNoSideEffects() {
   MyClass systemUnderTest = makeMyClass();
   MyClass copyOfOriginalState = systemUnderTest.clone();
   systemUnderTest.myFunction();
   assertEquals(systemUnderTest, copyOfOriginalState); //Test equals() method elsewhere
}

尝试证明一种方法是真正无副作用的,这有点不寻常。单元测试通常试图证明方法行为正确且符合合同,但它们并不意味着要替换检查代码。检查方法是否有任何可能的副作用通常是一项非常简单的练习。如果你的方法从不设置字段的值并且从不调用任何非功能性方法,那么它就是有用的。

在运行时测试这个很棘手。可能更有用的是某种静态分析。也许您可以创建一个@Functional注释,然后编写一个程序来检查程序的类以查找这些方法,并检查它们是否只调用其他@Functional方法而从不分配给字段。

随机搜索,我在这个主题上找到了某人的master's thesis。也许他有可用的工作代码。

不过,我会重申,我的建议是将注意力集中在其他地方。虽然你可以大多证明一种方法根本没有副作用,但在许多情况下通过视觉检查快速验证这一点可能会更好,并将剩余的时间集中在其他更基本的测试上。

答案 4 :(得分:0)

我担心我不再找到链接了,但是Junit 4有一些帮助功能来生成testdata。就像:

public void testData() {
  data = {2, 3, 4};
  data = {3,4,5 };
...
 return data;
}
然后,Junit会将您的方法用于此数据。但正如我所说,我不能再找到链接(忘记关键字)的详细(和正确)示例。

答案 5 :(得分:0)

查看http://fitnesse.org/:它经常被用于验收测试,但我发现这是一种对大量数据运行相同测试的简单方法

答案 6 :(得分:0)

在junit中,您可以编写自己的测试运行器。此代码未经过测试(我不确定获取参数的方法是否会被识别为测试方法,是否需要更多的转轮设置?):

public class MyRunner extends BlockJUnit4ClassRunner {

    @Override
    protected Statement methodInvoker(final FrameworkMethod method, final Object test) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                Iterable<Object[]> permutations = getPermutations();
                for (Object[] permutation : permutations) {
                    method.invokeExplosively(test, permutation[0], permutation[1]);
                }
            }
        };
    }

}

应该只提供getPermutations()实现。例如,它可以从一些带有一些自定义注释的List<Object[]>字段中获取数据并生成所有排列。

答案 7 :(得分:0)

我认为您缺少的术语是“参数化测试”。然而,在.Net风格的jUnit中似乎更乏味。在NUnit中,以下测试对所有组合执行6次。

[Test]
public void MyTest(
    [Values(1,2,3)] int x,
    [Values("A","B")] string s)
{
    ...
}

对于Java,您的选项似乎是:

  • JUnit supports this版本4.然而,它是很多代码(似乎jUnit坚持不测试参数的测试方法)。这是侵入性最小的。
  • DDSteps,一个jUnit插件。请参阅this video,其中包含来自适当命名的Excel电子表格的值。您还需要编写一个mapper / fixture类,它将电子表格中的值映射到fixture类的成员,然后用于调用SUT。
  • 最后,你有Fit / Fitnesse。它与DDSteps一样好,除了输入数据是HTML / Wiki形式的事实。您可以从Excel工作表粘贴到Fitnesse,只需按一下按钮即可正确格式化。你也需要在这里写一个夹具类。