Equivalent to @DirtiesContext(...) for Surefire + JUnit?
Asked Answered
E

1

2

I'm using the maven-surefire-plugin with junit 4.1.4. I have a unit test which relies on a 3rd party class that internally uses static { ... } code block to initiate some variables. For one test, I need to change one of these variables, but only for certain tests. I'd like this block to be re-executed between tests, since it picks up a value the first time it runs.

When testing, it seems like surefire instantiates the test class once, so the static { ... } code block is never processed again.

This means my unit tests that change values required for testing are ignored, the static class has already been instantiated.

πŸ’­ Note: The static class uses System.loadLibrary(...), from what I've found, it can't be rewritten to be instantiated, static is the (rare, but) proper usage.

I found a similar solution for Spring Framework which uses @DirtiesContext(...) annotation, allowing the programmer to mark classes or methods as "Dirty" so that a new class (or in many cases, the JVM) is initialized between tests.

How do you do the same thing as @DirtiesContext(...), but with maven-surefire-plugin?

public class MyTests {
    @Test
    public void test1() {
        assertThat(MyClass.THE_VALUE, is("something-default"));
    }

    @Test
    public void test2() {
        System.setProperty("foo.bar", "something-else");
        assertThat(MyClass.THE_VALUE, is("something-else"));
        //                            ^-- this assert fails
        //                                value still "something-default"
    }
}
public class MyClass {
    static {
        String value;
        if(System.getProperty("foo.bar") != null) {
            value = System.getProperty("foo.bar"); // set to "something-else"
        } else {
            value = "something-default";
        }
    }
    public static String THE_VALUE = value;

}
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>4.1.2</version>
      </plugin>
Eastward answered 4/8, 2021 at 20:46 Comment(15)
Surefire has very little to do with it - it’s the test engine that controls the lifecycle of test classes (JUnit, JUnit5 etc). You can ask surefire to fork out multiple JVMs - but that’s slow. – Fluxion
@BoristheSpider I updated the question to specify that I'm using Junit 4. Does that help clarify the question? I'm ok forking another JVM, it's how this would be done in practice. In regards to surefire-maven-plugin versus JUnit, I'm mostly just using @Test annotation for these, so I'm still unsure how to fix the static class problem. I've also added a pseudocode snippet. – Eastward
Also, reuseForks set to false seems to be broken: issues.apache.org/jira/browse/SUREFIRE-1534. Edit: Bumping to Surefire 3.0.0-M4 fixes this. – Eastward
Unfortunately bumping to Surefire 3.0.0-M4 and setting reuseForks false doesn't fix this, since it's the same test class, it still fails. Is there another way? – Eastward
Can't you put the test methods in different test classes so that <reuseForks>'s "If set to "false", a new VM is forked for each test class to be executed." applies? – Pasley
@GeroldBroser Yes, but this is a small project with only 2 unit tests. Creating separate classes is a bit overkill. – Eastward
I may take this opportunity to just remove the static initializer, but I've seen enough projects to leverage them that I don't see this problem going away. – Eastward
Ok... unfortunately this class uses System.loadLibrary(...) so the static invocation is static to the JVM regardless of the class design. I'll move the test to another class for now, unless there are some other ideas. – Eastward
Re "overkill": It doesn't matter how...small...your code is if it doesn't work. ;) – Pasley
I don't think it's polite to minimize things like this. If one needs to run 12 unit tests that impact this same static block, it requires 12 separate classes to do so. It also affects @Test reusability for each class, which is especially grotesque (or more accurately, inflated) in scenarios where tests are otherwise identical. I chose the word "overkill" as a short way to describe the impact of this design. I think it fairly accurately represents how onlooking developers would feel if put the same situation. – Eastward
Please relax. And see the ;) ... no attack was intended. I can delete the comment if you want. – Pasley
Please leave it, it adds context to the following question. – Eastward
I've added a note to the question about System.loadLibrary(...) to explain why static is (rarely but in this case correctly) being used. – Eastward
Maybe this answer to SuppressStaticInitializationFor(Powermock) is helpful. – Pasley
Quoting: "PowerMock is a Java framework that allows you to unit test code normally regarded as untestable.". Yeah, this is probably the framework to use. It does use bytecode injection techniques, which seems like it could add its own issues (possible blocking newer JDKs too), but sharing knowledge of the framework is greatly appreciated. It seems to be exactly for this type of use-case. – Eastward
C
0

static initialization blocks in java are something that can't be easily handled by JUnit. In general static stuff doesn't play nicely with unit testing concepts.

So, assuming you can't touch this code, your options are:

Option 1:

Spawn a new JVM for each test - well, this will work, but might be an overkill because it will aggravate the performance

If you'll follow this path, you might need to configure surefire plugin with:

forkCount=1
reuseForks=false

According to the surefire plugin documentation this combination will execute each test class in its own JVM process.

Option 2:

Create a class with a different class loader for every test.

Basically in Java if class com.foo.A is created by ClassLoader M is totally different than the same class com.foo.A created by ClassLoaded N. This is somewhat hacky but should work.

The overhead is much smaller than in option 1. However you'll have to understand how to "incorporate" new class loaders into the testing infrastructure.

For more information about the creation of the custom class loader read for example this tutorial

Cobbett answered 5/8, 2021 at 15:50 Comment(3)
Thanks. The first solution is discussed in detail the OP's comments, but for completeness sake: "if you can't touch this code" the code can be touched, but the class uses System.loadLibrary(...) which is EFFECTIVELY static, so reinstantiation doesn't do anything, so the choice was made to leave it alone as it's actually more accurate. For now, adding tests into separate classes and using <reuseFork>false</reuseFork> is the workaround, but I do not consider this a solution. In regards to the class loader, I agree, its hacky and seems more confusing from a unit testing perspective. – Eastward
"In general static stuff doesn't play nicely with unit testing concepts." I don't understand this statement. Something as simple as java.library.path is effectively static and can impact many (many) projects, so this banish all static pragmatism hurts projects which use real-world, recommended techniques. I'd be happy to reinvoke all unit tests in a for-loop with differentiating properties if that's a possibility. This notion that nothing can be static holds weight for some stuff, not others. Making new classes each time is not the solution, rather a workaround for a plugin behavior. – Eastward
Note, I've also entertained something like System.unloadLibrary(...) paradigm to allow a proper un-instantiation but I could not find such an API.:) I've updated the question to explain why static is (rare but properly) being used. – Eastward

© 2022 - 2024 β€” McMap. All rights reserved.