60

JUnit4, JUnit5, and Spock: A Comparison - DZone Java

 6 years ago
source link: https://dzone.com/articles/junit4-junit5-and-spock-framework-comparison
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

JUnit4, JUnit5, and Spock: A Comparison

This summary of a JUG meeting on unit testing compares JUnit 4, JUnit 5, and the Spock framework to see what features they offer.

Jan. 15, 18 · Tutorial
Like (36)
29.06K Views

Recently, I gave a talk in my local Java User Group about unit testing. Some of the content of the talk was about some popular libraries you can use in your Java project. I’ve reviewed JUnit4, JUnit5, and the Spock framework. Many of the attendees were quite surprised with the differences. In this post, I will summarize asserts, parametrized tests, and mocking.

I always like to demonstrate the concepts with examples and live coding, so I chose a simple algorithm: a Fibonacci number calculator. If you don’t know it, it’s just to generate numbers that are the sum of the two previous ones in the series: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377.

I used the typical (and very badly performing) implementation:

private static int fibonacci(int n) {
    if (n <= 1) return n; else
        return fibonacci(n-1) + fibonacci(n-2);
}

JUnit4

I started by explaining JUnit4 because it’s the most popular library and the base for many others. I started explaining assertTrue and then more advances usages, including assertEquals.

The Latest DZone Refcard

Getting Started With Apache Iceberg

thumbnail?fid=16082369&w=350
@Test
public void improvedFibonacciTestSimple() {
    FibonacciWithJUnit4 f = new FibonacciWithJUnit4();
    assertEquals(f.fibonacci(4), 3);
}

If it’s false, it would give you an error like:

java.lang.AssertionError:
Expected :3
Actual   :2

Not very spectacular, but quite useful.

The next thing was to show how to write a parameterized test a nice, very useful feature for test algorithms. In JUnit4, that is quite tricky. You need to create a Collection with the annotation @Parameters.

@Parameters
public static Collection < Object[] > data() {
    return Arrays.asList(new Object[][] {
        {
            0,
            0
        }, {
            1,
            1
        }, {
            2,
            1
        }, {
            3,
            2
        }, {
            4,
            3
        }, {
            5,
            5
        }, {
            6,
            8
        }
    });
}

Then we create some local variables and a constructor:

private int fInput;
private int fExpected;

public ParametrizedFibonacciJUnit4(int input, int expected) {
    fInput = input;
    fExpected = expected;
}

And finally, we can use assertEquals:

@Test
public void test() {
    FibonacciWithJUnit4 f = new FibonacciWithJUnit4();
    assertEquals(fExpected, f.fibonacci(fInput));
}

It’s quite verbose, and if the test fails, you would get a message that doesn’t clearly indicate the order or the parameters used (but your IDE will probably help on that):

java.lang.AssertionError:
Expected :0
Actual   :1

I didn’t explain mocking here because an external library like Mockito is usually required when you want to use mocking with JUnit4. Mockito is great, but I didn’t have enough time to explain it.

JUnit5

JUnit5 was considered stable in September 2017, and it should be your choice over JUnit4 for several reasons. It has better support for Java 8 and lambdas, it’s compatible with JUnit4 (you can have both, which is great to migrate in a progressive way), and it provides new runners and better integrations.

I repeated the same process. First, show an assertEquals:

@Test
public void bestFibonacciTestSimple() {
    FibonacciWithJUnit5 f = new FibonacciWithJUnit5();
    Assertions.assertEquals(f.fibonacci(4), 3);
}

There are important advances in other asserts, for instance the timeout, but for assertEqual with integers, it’s practically the same. Also, the message is similar if there is an error:

org.opentest4j.AssertionFailedError:
Expected :3
Actual   :2

We can find important changes in parametrized tests. First, we need to use the @ParametrizedTest annotation, and we can specify a name using { and } to indicate important parameters like index with arguments.

@ParameterizedTest(name = "run #{index} with [{arguments}]")

Now we can define our test. We start defining the entries to test function with the annotation @CsvSource. Each item will replace the parameters in the test function, in this case, input and expected.

@CsvSource({"1, 1", "4, 3"})
    public void test2(int input , int expected) {
        FibonacciWithJUnit5 f = new FibonacciWithJUnit5();
        Assertions.assertEquals(f.fibonacci(input), expected);
}

This is a lot better than the JUnit4 implementation. Also, if there is a failure in the test, we obtain a better message indicating the difference, the index causing the failure, and the used parameters.

Finally, it’s the same for mocking as JUnit4: You normally use an external library, so I didn’t explain it.

Spock

The last topic was the Spock framework. It’s based on Apache Groovy. It allows you to write less code in a clearer way. It’s very powerful. Some years ago, we started to use it for "non-critical" development: tests, dependency management, Continuous Integration, load testing, and in any place where we needed some configuration file avoiding XML, JSON, or any format like that. We continue to develop the core of our software in Java, and it isn’t a problem because both languages play very well together. If you know Java, you know Groovy… so we have the best of both worlds.

Writing a test in Spock is quite different. It would be like this:

def "Simple test"() {
    setup:
    BadFibonacci f = new BadFibonacci()

    expect:
    f.fibonacci(1) == 1
    assert f.fibonacci(4) == 3
}

Basically, we can use def where we don’t care about the type. The name of the function can be defined between quotation marks, which allows us to use better naming for our tests. We have some special words such as setup, when, expect, and, etc. to define our tests in a more descriptive and structured way. And we have a power assert, which is part of the language itself:

Condition not satisfied:

f.fibonacci(4) == 2
| |            |
| 3            false
BadFibonacci@23c30a20

Expected :2

Actual   :3

It provides all the information: the returned value (actual), the expected value, the function, the parameter, etc. Using assert in Groovy is really handy.

Now for the parametrized tests. One would look something like this:

def "parametrized test"() {
    setup:
    BadFibonacci f = new BadFibonacci()

    expect:
    f.fibonacci(index) == fibonacciNumber

    where:
    index | fibonacciNumber
    1     | 1
    2     | 1
    3     | 2
}

After I showed this, I heard some 'oooh's in the audience. The magic of this code is that you don’t need to explain it! There is a table in the where: section, and the values in expect: are automagically replace it in each iteration. If there is a failure, the message is crystal clear:

Condition not satisfied:

f.fibonacci(index) == fibonacciNumber
| |         |      |  |
| 2         3      |  4
|                  false
BadFibonacci@437da279

Expected :4

Actual   :2

Then I very quickly introduced mocks and stubs. A mock is an object you create in your test to avoid using a real object. For example, you don’t want to do real web requests or print a page in your tests, so you can use a mock from an interface or another object.

Subscriber subscriber = Mock()

def "mock example"() {
    when:
    publisher.send("hello")

    then:
    1 * subscriber.receive("hello")

Basically, you create the subscriber interface as a mock, then you can invoke the methods. The 1 * is another nice feature of Spock — it specifies how many messages you should receive. Cool, right?

On some occasions, you need to define what returns the methods of your mocks. For that, you can create a stub.

def "stub example"() {
    setup:
    subscriber.receive(_) >> "ok"

    when:
    publisher.send("message1")

    then:
    subscriber.receive("message1") == 'ok'
}

In this case, with the >> notation, we are defining that the method receive should return ok independently of the parameter (_ means any value). The test passes without any problem.

Conclusions

I don’t like to recommend one library or another: All of them have their use cases. It’s pretty clear we have great options in Java, and I just gave some examples. Now it’s your turn to decide which is better for you. The only thing I can say is: Write tests and master your library of choice. It will make you a better developer!

If you want to take a deeper look at the examples, you will find them in this GitHub repository. Enjoy!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK