1

I was learning JUnit5 but I got distracted with the concept of functional programming. So far I could understand why for a method like dynamicTest() I can not use dynamicTest(str, assertEquals(a, multiply(b,c)) instead of dynamicTest(str, () -> assertEquals(a, multiply(b,c)).

"... because dynamicTest() needs the execution of assertEquals() as the second arg and not the result of assertEquals()."

But I cant understand, why a method would need the execution of another method as its argument. I would need an explanation with a simple example, thanks.

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

import java.util.Arrays;
import java.util.stream.Stream;

import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;

public class DynamicTestCreationTest {

    @TestFactory
    public Stream<DynamicTest> testMultiplyException() {
        MyClass tester = new MyClass();
        int[][] data = new int[][] { { 1, 2, 2 }, { 5, 3, 15 }, { 121, 4, 484 } };
        return Arrays.stream(data).map(entry -> {
            int m1 = entry[0];
            int m2 = entry[1];
            int expected = entry[2];
            return dynamicTest(m1 + " * " + m2 + " = " + expected, () -> {
                assertEquals(expected, tester.multiply(m1, m2));
            });
        });
    }

    // class to be tested
    class MyClass {
        public int multiply(int i, int j) {
            return i * j;
        }
    }
}
Mori
  • 107
  • 9
  • That's what makes it dynamic. You can pass operations instead of just values. – Kayaman Sep 20 '19 at 10:06
  • @Kayaman how does it make dynamic? All the variables used in that lambda are static. – Andrew Tobilko Sep 20 '19 at 10:13
  • @AndrewTobilko i think it is dynamic because by iterating every time you take a new inner array so m1, m2 and expected take new values and that makes it dynamic... isn't it? – Mori Sep 20 '19 at 10:17
  • @Mori your `int[][] data` is known at compile-time, we can't call this input dynamic, can we? – Andrew Tobilko Sep 20 '19 at 10:20
  • 1
    When creating `DynamicTest` instances, you don't want the actual code-to-be-tested invoked immediately. What you're doing is _creating the tests_ which will be executed _at a later time_. Thus you provide an object (in this case, an instance of `Executable`) that encapsulates the test and, when ready, some internal code invokes the appropriate method on that object (e.g. `Execuatable#execute()`). – Slaw Sep 20 '19 at 10:20
  • @AndrewTobilko yeah i think your right , iteration does not make it dynamic... – Mori Sep 20 '19 at 10:23
  • That’s an alternative to, e.g. `@RunWith(Parameterized.class)`/`@Parameters`. Sure, you could write the code to invoke the test methods directly with the changing parameter, but in the end, that applies to the entire JUnit framework. You could write the code to run through all tests yourself and it wouldn’t be so hard, as it’s just a bunch of nested loops. But I suppose, you’ll find yourself one or another reason of letting a framework do the job… – Holger Sep 20 '19 at 10:26
  • @Holger I absolutely prefer JUnit 4.12 and its three approaches for a parameterized test ( with Constructor , with `@Parameter` and the faster one `@Parameters({"a1, b1, c1" , "a2, b2, c2" , ...} )` ) comparing to JUnit 5 , but i just wanted to understand this ufo things lol – Mori Sep 20 '19 at 10:34
  • Have you seen `@ParameterizedTest`? It allows you to inject arguments into a test method based on configurable argument providers. I'd link to documentation but the JUnit website appears to be a part of the "_Global #ClimateStrike_" until Sept. 21. – Slaw Sep 20 '19 at 10:40
  • This Q&A demonstrates parameterized tests in JUnit 5: [How to implement JUnit 4 parameterized tests in JUnit 5?](https://stackoverflow.com/questions/46897134/how-to-implement-junit-4-parameterized-tests-in-junit-5) – Slaw Sep 20 '19 at 10:45
  • @Slaw thank you! trying to understand... – Mori Sep 20 '19 at 10:47
  • @Slaw yeah i have seen that and as i said i prefer JUnit 4.12 but im trying to understand what happens in a xMethod( Executalbe executable) and why is it good... and i think those parts of your comment helps : "you dont want..." and "what you are doing..." – Mori Sep 20 '19 at 10:55
  • @Slaw their website works when you disable JavaScript. – Holger Sep 20 '19 at 11:27
  • @Slaw I have just two questions: 1. By "actual code-to-be-tested" do you mean "actual code-to-be-tested-evaluation" (tests)? because i think code-to-be-tested = multiply(x,y) is invoked immediately , but it's the assertion() which waits, am I right? 2. And what is the point of executing-later? what is the advantage of later and not immediately? what are the method waiting for? – Mori Sep 20 '19 at 17:37
  • @Slaw sorry, i think my 2nd question is nonsense and the answer to it is: The method is waiting for my click on "Run". – Mori Sep 20 '19 at 18:17

2 Answers2

0

But I cant understand, why a method would need the execution of another method as its argument. I would need an explanation with a simple example, thanks.

Maybe I'm not understanding your question correctly, but in short it does not, you can always execute the method outside of the arguments and assign that output to a parameter, which you in-turn pass as an argument, for example:

@Test
public void test_oddMethod() {
    OddClass oddClass = new OddClass();
    boolean expected = Boolean.TRUE;
    boolean methodOutput = oddClass.isOdd();
    assertEquals(expected, methodOutput);
}

One reason why you'd execute a method call as part of a parameter is simply to reduce the lines of code and make your method more "readable". In the example above, there is no real reason to declare the boolean methodOutput as it's only used once as part of assertEquals(...), hence this can be simplified to:

@Test
public void test_oddMethod() {
    OddClass oddClass = new OddClass();
    boolean expected = Boolean.TRUE;
    assertEquals(expected, oddClass.isOdd());
}

You could simplify this even further:

@Test
public void test_oddMethod() {
    assertEquals(Boolean.TRUE, new OddClass().isOdd());
}
Ambro-r
  • 919
  • 1
  • 4
  • 14
  • I think there's a misunderstanding... the question is generally about a method with an `Executable executable` argument. (in this case the method is dynamicTest() and its argument `() -> assertEquals(...)` thx anyway :) – Mori Sep 20 '19 at 11:04
  • Ok. That's simply a block of code you want executed on each sequential element that is being streamed from either an Array or Collection. `stream.map(...)` returns a stream consisting of the results of applying the given execution code to the elements of this stream. In this case you are streaming each element in `int[][] data`, so the first element will be _{ 1, 2, 2 }_, you are then dynamically creating a test called _1 * 2 = 2_ and for this test you executing your assertion. – Ambro-r Sep 20 '19 at 16:55
0

Here's what the JUnit 5 User Guide has to say about Dynamic Tests:

§2.17. Dynamic Tests

The standard @Test annotation in JUnit Jupiter described in Annotations is very similar to the @Test annotation in JUnit 4. Both describe methods that implement test cases. These test cases are static in the sense that they are fully specified at compile time, and their behavior cannot be changed by anything happening at runtime. Assumptions provide a basic form of dynamic behavior but are intentionally rather limited in their expressiveness.

In addition to these standard tests a completely new kind of test programming model has been introduced in JUnit Jupiter. This new kind of test is a dynamic test which is generated at runtime by a factory method that is annotated with @TestFactory.

In contrast to @Test methods, a @TestFactory method is not itself a test case but rather a factory for test cases. Thus, a dynamic test is the product of a factory. Technically speaking, a @TestFactory method must return a single DynamicNode or a Stream, Collection, Iterable, Iterator, or array of DynamicNode instances. Instantiable subclasses of DynamicNode are DynamicContainer and DynamicTest. DynamicContainer instances are composed of a display name and a list of dynamic child nodes, enabling the creation of arbitrarily nested hierarchies of dynamic nodes. DynamicTest instances will be executed lazily, enabling dynamic and even non-deterministic generation of test cases.

[...]

A DynamicTest is a test case generated at runtime. It is composed of a display name and an Executable. Executable is a @FunctionalInterface which means that the implementations of dynamic tests can be provided as lambda expressions or method references.

Dynamic Test Lifecycle

The execution lifecycle of a dynamic test is quite different than it is for a standard @Test case. Specifically, there are no lifecycle callbacks for individual dynamic tests. This means that @BeforeEach and @AfterEach methods and their corresponding extension callbacks are executed for the @TestFactory method but not for each dynamic test. In other words, if you access fields from the test instance within a lambda expression for a dynamic test, those fields will not be reset by callback methods or extensions between the execution of individual dynamic tests generated by the same @TestFactory method.

[...]

As explained, a dynamic test is generated at runtime and is represented by a DynamicTest object. This means when you have a @TestFactory method you are creating tests, not executing them. In order to support lazy execution you need to encapsulate the actual test in an object, which is done with Executable. It may help to imagine a single DynamicTest as a "normal" @Test. Say you have:

@TestFactory
DynamicTest generateDynamicTest() {
    return DynamicTest.dynamicTest(
            "2 + 2 = 4", 
            () -> assertEquals(4, 2 + 2, "the world is burning")
    );
}

As a @Test method the above would look like:

@Test
@DisplayName("2 + 2 = 4")
void testMath() {
    assertEquals(4, 2 + 2, "the world is burning");
}

Note: The two are not quite equivalent. As mentioned in the user guide, dynamic tests do not have the same lifecycle as normal @Test methods—read the guide to understand the differences.

In other words, the Executable is the body of the test method. You can think of a @TestFactory as generating a bunch of test methods at runtime (conceptually). So when you wrap your test code in an Executable you are creating a function and passing the function to the framework. This allows dynamic tests to mimic the behavior of non-dynamic tests and let's the framework execute the tests when it's ready to do so.

To answer your two additional questions you put in a comment:

  1. By "actual code-to-be-tested" do you mean "actual code-to-be-tested-evaluation" (tests)? because i think code-to-be-tested = multiply(x,y) is invoked immediately , but it's the assertion() which waits, am I right?

    The wording of "code-to-be-tested" is, I now realize, ambiguous if not just misleading. Yes, I mean the test code (i.e. the code wrapped in the Executable, such as the assertions) is what you don't want to be invoked immediately, but rather at some later time—when the test framework is ready to execute the test.

    Note you potentially have "double the laziness" in your example due to the use of Stream<DynamicTest>. Since a Stream is evaluated lazily, and you don't eagerly build the Stream (e.g. with Stream.of), it only creates the DynamicTest objects as they're needed. This can be beneficial if creating the DynamicTest is expensive because creating all the tests up front can be avoided. Whether or not JUnit Jupiter takes advantage of this, I'm not sure (haven't looked at the implementation), though I'd be surprised if they didn't.

  2. And what is the point of executing-later? what is the advantage of later and not immediately? what are the method waiting for?

    The DynamicTest is waiting to be passed to the framework and then waiting for the framework to execute it (and executing the DynamicTest involves executing the Executable).

    Remember, we're dealing with a test factory here, which means you are creating the tests and not executing the tests. Executing the tests is the responsibility of the framework. If the Executable was executed eagerly then it'd be you executing the test instead of the framework. In effect, eager execution would hide each DynamicTest inside the @TestFactory method, preventing the framework from seeing them as individual tests; the framework has to know which test it's executing in order to give an accurate report. Plus, if eagerly executed, a test failure would prevent any remaining tests from being executed.


Note the example in your question could also be accomplished with a parameterized test.

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

class MultiplicationTests {

    static Stream<Integer[]> numbersProvider() {
        return Stream.of(
                new Integer[]{1, 2, 2},
                new Integer[]{5, 3, 15},
                new Integer[]{121, 4, 484}
        );
    }

    @ParameterizedTest(name = "{0} * {1} = {2}")
    @MethodSource("numbersProvider")
    void testMultiplication(int a, int b, int expectedResult) {
        assertEquals(expectedResult, a * b);
    }

}

Of course, this just seems like another way to do the same thing. So what's the difference between a @ParameterizedTest and a @TestFactory?

  • A parameterized test goes through the normal lifecycle for each invocation.
  • With a test factory, the entire test can be dynamically generated, not just the parameters. You could probably mimic this with a parameterized test but you'd be fighting against the design.
  • With a test factory, you're literally creating tests; a parameterized test already exists, you're just providing different parameters for each invocation.

At least, that's how I understand the differences.

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • thank you so much for the comprehensive answer, because of my low reputation i couldnt vote your great answer. It is not the last time i read your answer here, it's worth to read multiple of times, thx for the effort again. :) – Mori Sep 25 '19 at 19:46