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

发布于 2024-09-13 14:46:42 字数 1751 浏览 11 评论 0原文

函数(无副作用的函数)是一个基本的构建块,但我不知道在 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,但这似乎不是解决方案。它被记录为生成器。我不是在寻找一种为我的函数生成输入的方法,而是一个框架,它允许我以声明方式指定对象的哪一部分是无副作用的,并使用基于该声明的一些排列来调用我的输入/输出规范。

更新:我不想验证对象中没有任何变化,记忆功能是此类测试的典型用例,并且记忆器实际上改变了其内部状态。然而,给定一些输入的输出始终保持不变。

Functions (side-effect free ones) are such a fundamental building block, but I don't know of a satisfying way of testing them in Java.

I'm looking for pointers to tricks that make testing them easier. Here's an example of what I want:

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);
}

I'm looking for some annotation I can put on the test(s), MyObject or myFunction to make the test framework automatically repeat invocations to myFunction in all possible permutations for the given input/output combinations I've given, or some subset of the possible permutations in order to prove that the function is functional.

For example, above the (only) two possible permutations are:

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

and:

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

I should be able to only provide the input/output pairs (someInput, expectedOutput), and (someOtherInput, someOtherOutput), and the framework should do the rest.

I haven't used QuickCheck, but it seems like a non-solution. It is documented as a generator. I'm not looking for a way to generate inputs to my function, but rather a framework that lets me declaratively specify what part of my object is side-effect free and invoke my input/output specification using some permutation based on that declaration.

Update: I'm not looking to verify that nothing changes in the object, a memoizing function is a typical use-case for this kind of testing, and a memoizer actually changes its internal state. However, the output given some input always stays the same.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

旧情别恋 2024-09-20 14:46:42

如果您试图测试函数是否无副作用,那么使用随机参数调用并不能真正解决问题。这同样适用于具有已知参数的随机调用序列。或者伪随机,具有随机或固定种子。有一个很好的机会是,(有害的)副作用只会发生在随机生成器选择的任何调用序列中。

还有一种可能是,无论输入是什么,副作用实际上不会在您正在进行的任何调用的输出中可见。它们的副作用可能会出现在您不想检查的其他一些相关对象上。

如果您想测试这种事情,您确实需要实现一个“白盒”测试,在其中查看代码并尝试找出可能导致(不需要的)副作用的原因,并根据该知识创建测试用例。但我认为更好的方法是仔细的手动代码检查,或使用自动静态代码分析器......如果您能找到一个可以为您完成这项工作的方法。

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

If you are trying to test that the functions are side-effect free, then calling with random arguments isn't really going to cut it. The same applies for a random sequence of calls with known arguments. Or pseudo-random, with random or fixed seeds. There's a good chance are that a (harmful) side-effect will only occur with any of the sequence of calls that your randomizer selects.

There is also a chance that the side-effects won't actually be visible in the outputs of any of the calls that you are making ... no matter what the inputs are. They side-effects could be on some other related objects that you didn't think to examine.

If you want to test this kind of thing, you really need to implement a "white-box" test where you look at the code and try and figure out what might cause (unwanted) side-effects and create test cases based on that knowledge. But I think that a better approach is careful manual code inspection, or using an automated static code analyser ... if you can find one that would do the job for you.

OTOH, if you already know that the functions are side-effect free, implementing randomized tests "just in case" is a bit of a waste of time, IMO.

不寐倦长更 2024-09-20 14:46:42

我不太确定我明白你在问什么,但看起来像 Junit Theories (http://junit.sourceforge.net/doc/ReleaseNotes4.4.html#theories)可能是一个答案。

I'm not quite sure I understand what you are asking, but it seems like Junit Theories (http://junit.sourceforge.net/doc/ReleaseNotes4.4.html#theories) could be an answer.

(り薆情海 2024-09-20 14:46:42

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

下面是这种附加的可能功能测试的简单示例:

@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());
   }
}

可以根据命令模式解决具有较高复杂性的问题:您可以将测试方法包装在命令对象中,将命令对象添加到列表中,对列表进行打乱并执行根据该列表的命令(=嵌入式测试)。

In this example, you could create a Map of key/value pairs (input/output) and call the method under test several times with values picked from the map. This will not prove, that the method is functional, but will increase the probability - which might be sufficient.

Here's a quick example of such an additional probably-functional test:

@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());
   }
}

Problems with a higher complexity could be solved based on the Command pattern: You could wrap the test methods in command objects, add the command object to a list, shuffle the list and execute the commands (= the embedded tests) according to that list.

权谋诡计 2024-09-20 14:46:42

听起来您正在尝试测试在类上调用特定方法不会修改其任何字段。这是一个有点奇怪的测试用例,但完全有可能为其编写一个清晰的测试。对于其他“副作用”,例如调用其他外部方法,则有点困难。您可以用测试存根替换本地引用并验证它们是否未被调用,但您仍然无法以这种方式捕获静态方法调用。尽管如此,通过检查来验证您没有在代码中执行类似的操作还是很简单的,有时这已经足够好了。

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

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

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

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

随机谷歌搜索,我发现有人的硕士论文正是关于这个主题的。也许他有可用的工作代码。

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

It sounds like you're attempting to test that invoking a particular method on a class doesn't modify any of its fields. This is a somewhat odd test case, but it's entirely possible to write a clear test for it. For other "side effects", like invoking other external methods, it's a bit harder. You could replace local references with test stubs and verify that they weren't invoked, but you still won't catch static method calls this way. Still, it's trivial to verify by inspection that you're not doing anything like that in your code, and sometimes that has to be good enough.

Here's one way to test that there are no side effects in a call:

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

It's somewhat unusual to try to prove that a method is truly side effect free. Unit tests generally attempt to prove that a method behaves correctly and according to contract, but they're not meant to replace examining the code. It's generally a pretty easy exercise to check whether a method has any possible side effects. If your method never sets a field's value and never calls any non-functional methods, then it's functional.

Testing this at runtime is tricky. What might be more useful would be some sort of static analysis. Perhaps you could create a @Functional annotation, then write a program that would examine the classes of your program for such methods and check that they only invoke other @Functional methods and never assign to fields.

Randomly googling around, I found somebody's master's thesis on exactly this topic. Perhaps he has working code available.

Still, I will repeat that it is my advice that you focus your attention elsewhere. While you CAN mostly prove that a method has no side effects at all, it may be better in many cases to quickly verify this by visual inspection and focus the remainder of your time on other, more basic tests.

甜点 2024-09-20 14:46:42

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

have a look at http://fitnesse.org/: it is used often for Acceptance Test but I found it is a easy way to run the same tests against huge amount of data

最终幸福 2024-09-20 14:46:42

在 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 字段中获取数据并生成所有排列。

In junit you can write your own test runner. This code is not tested (I'm not sure if methods which get arguments will be recognized as test methods, maybe some more runner setup is needed?):

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]);
                }
            }
        };
    }

}

It should be only a matter of providing getPermutations() implementation. For example it can take data from some List<Object[]> field annotated with some custom annotation and produce all the permutations.

哽咽笑 2024-09-20 14:46:42

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

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

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

  • JUnit 在版本 4 中支持此功能。但是,代码量很大(看来 jUnit 坚持不带参数的测试方法)。这是侵入性最小的。
  • DDSteps,一个 jUnit 插件。请观看此视频,该视频从适当命名的 Excel 电子表格中获取值。您还需要编写一个映射器/夹具类,将电子表格中的值映射到夹具类的成员中,然后用于调用 SUT。
  • 最后,您还有 Fit/Fitnesse。它与 DDSteps 一样好,只是输入数据是 HTML/Wiki 形式。您可以将 Excel 工作表粘贴到 Fitnesse 中,只需按一下按钮即可正确设置格式。您还需要在这里编写一个固定类。

I think the term you're missing is "Parametrized Tests". However it seems to be more tedious in jUnit that in the .Net flavor. In NUnit, the following test executes 6 times with all combinations.

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

For Java, your options seem to be:

  • JUnit supports this with version 4. However it's a lot of code (it seems, jUnit is adamant about test methods not taking parameters). This is the least invasive.
  • DDSteps, a jUnit plugin. See this video that takes values from appropriately named excel spreadsheet. You also need to write a mapper/fixture class that maps values from the spreadsheet into members of the fixture class, that are then used to invoke the SUT.
  • Finally, you have Fit/Fitnesse. It's as good as DDSteps, except for the fact that the input data is in HTML/Wiki form. You can paste from an excel sheet into Fitnesse and it formats it correctly at the push of a button. You need to write a fixture class here too.
赠佳期 2024-09-20 14:46:42

恐怕我找不到链接了,但是 Junit 4 有一些帮助函数来生成测试数据。就像:

public void testData() {
  data = {2, 3, 4};
  data = {3,4,5 };
...
 return data;
}

Junit 将使用这些数据来处理你的方法。但正如我所说,我无法再找到详细(且正确)示例的链接(忘记了关键字)。

Im afraid that I dont find the link anymore, but Junit 4 has some help functions to generate testdata. Its like:

public void testData() {
  data = {2, 3, 4};
  data = {3,4,5 };
...
 return data;
}

Junit will then thest your methods will this data. But as I said, I cant' find the link anymore (forgot the keywords) for a detailed (and correct) example.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文