13

I have written some unit tests for a static method. The static method takes only one argument. The argument's type is a final class. In terms of code:

public class Utility {

   public static Optional<String> getName(Customer customer) {
       // method's body.
   }
}

public final class Customer {
   // class definition
}

So for the Utility class I have created a test class UtilityTests in which I have written tests for this method, getName. The unit testing framework is TestNG and the mocking library that is used is Mockito. So a typical test has the following structure:

public class UtilityTests {

   @Test
   public void getNameTest() {
     // Arrange
     Customer customerMock = Mockito.mock(Customer.class);
     Mockito.when(...).thenReturn(...);

     // Act
     Optional<String> name = Utility.getName(customerMock);

     // Assert
     Assert.assertTrue(...);
   }
}

What is the problem ?

Whereas the tests run successfully locally, inside IntelliJ, they fail on Jenkins (when I push my code in the remote branch, a build is triggered and unit tests run at the end). The error message is sth like the following:

org.mockito.exceptions.base.MockitoException: Cannot mock/spy class com.packagename.Customer Mockito cannot mock/spy because : - final class

What I tried ?

I searched a bit, in order to find a solution but I didn't make it. I note here that I am not allowed to change the fact that Customer is a final class. In addition to this, I would like if possible to not change it's design at all (e.g. creating an interface, that would hold the methods that I want to mock and state that the Customer class implements that interface, as correctly Jose pointed out in his comment). The thing that I tried is the second option mentioned at mockito-final. Despite the fact that this fixed the problem, it brake some other unit tests :(, that cannot be fixed in none apparent way.

Questions

So here are the two questions I have:

  1. How that is possible in the first place ? Shouldn't the test fail both locally and in Jenkins ?
  2. How this can be fixed based in the constraints I mentioned above ?

Thanks in advance for any help.

Christos
  • 53,228
  • 8
  • 76
  • 108
  • 1
    My guess would be that the `enable final` configuration works in your workspace, but when run on `Jenkins` its unable to find this file. Check where `Jenkins` is looking for the file and whether its actually there or not. – second Nov 22 '19 at 11:39
  • This other thread explains how to enable final class mocking in Mockito 2, by adding a mockito configuration file under the resources directory: https://stackoverflow.com/questions/14292863/how-to-mock-a-final-class-with-mockito – Jose Tepedino Nov 25 '19 at 01:58
  • 3
    Would it be possible, in the code you're dealing with, to extract an interface from the Customer class, say ICustomer, and use it in the Utility class? Then you could mock that interface instead of the concrete final class – Jose Tepedino Nov 25 '19 at 02:04
  • @JoseTepedino This is a valid point. It does make sense totally and it's definitely an elegant way to overcome this problem. However I wonder if there is another way and more importantly, I want to understand why the current approach succeeds locally and fails in Jenkins. – Christos Nov 25 '19 at 02:09
  • @Christos, it is strange to be working locally; by default it should have also failed there. Does your project have a configuration file at `src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker`? (like https://www.baeldung.com/mockito-final) – Jose Tepedino Nov 25 '19 at 02:35
  • @JoseTepedino It hadn't. Then I added it but it didn't made the trick. – Christos Nov 25 '19 at 02:45
  • @Christos, I have sucessfully run the test with the final class, by adding that configuration file (hint: make sure its contents is `mock-maker-inline`); but I'm using JUnit, not TestNG. If that file doesn't exist, then it is still strange that Mockito is not emitting an error locally. – Jose Tepedino Nov 25 '19 at 02:55
  • 1
    Does `Customer` have any logic in it, or is it just a dumb data class? If it's just a bunch of fields with getters and setters, then you can just instantiate it. – Willis Blackburn Nov 25 '19 at 03:51
  • @WillisBlackburn It does contain some logic and it has a constructor that take a few arguments that are neither easily mocked or created. – Christos Nov 25 '19 at 13:27
  • I'm confused by this statement: "The thing that I tried is the second option mentioned at mockito-final. Despite the fact that this fixed the problem, ..." What is the "second thing?" – Willis Blackburn Nov 25 '19 at 15:24
  • A difference between your local config and Jenkins may be that locally your project is being built by IntelliJ while in Jenkins it's being built by... something else. Also, the classpath is possibly different; even if has the same jars/paths, they might be in a different order. Another issue I've seen with resource files is that the build is not configured to copy them into the class directory/jar. – Willis Blackburn Nov 25 '19 at 15:30
  • I would modify the test to print a ton of diagnostic information (system properties, which will include class path, environment variables, the version of Mockito, the result of an attempt to locate the resource mockito-extensions/org.mockito.plugins/MockMaker, etc.), then run the test in both environments and compare. – Willis Blackburn Nov 25 '19 at 15:31
  • Okay, it fails in Jenkins, but works fine in IDE. What is the result of test execution triggered from console? For maven you can use this command: https://maven.apache.org/surefire/maven-surefire-plugin/examples/single-test.html – The Rabbit of No Luck Dec 03 '19 at 19:34
  • How did you run the test? ran the single test class manually in IntelliJ OR did you run `mvn test`? – Sunil Dabburi Dec 04 '19 at 19:56

3 Answers3

2

An alternative approach would be to use the 'method to class' pattern.

  1. Move the methods out of the customer class into another class/classes, say CustomerSomething eg/CustomerFinances (or whatever it's responsibility is).
  2. Add a constructor to Customer.
  3. Now you don't need to mock Customer, just the CustomerSomething class! You may not need to mock that either if it has no external dependencies.

Here's a good blog on the topic: https://simpleprogrammer.com/back-to-basics-mock-eliminating-patterns/

Johnny Alpha
  • 758
  • 1
  • 8
  • 35
  • 1
    Thanks for your answer (+1). I found a way to fix it (answer to the second question). However the reason why the tests fail inside IntelliJ is still not clear to me. Furthermore, I can't reproduce it anymore (the failure inside the IntelliJ), which is totally weird. – Christos Dec 08 '19 at 12:47
1

How that is possible in the first place? Shouldn't the test fail both locally and in Jenkins ?

It's obviously a kind of env-specifics. The only question is - how to determine the cause of difference.

I'd suggest you to check org.mockito.internal.util.MockUtil#typeMockabilityOf method and compare, what mockMaker is actually used in both environments and why.

If mockMaker is the same - compare loaded classes IDE-Client vs Jenkins-Client - do they have any difference on the time of test execution.

How this can be fixed based in the constraints I mentioned above?

The following code is written in assumption of OpenJDK 12 and Mockito 2.28.2, but I believe you can adjust it to any actually used version.

public class UtilityTest {    
    @Rule
    public InlineMocksRule inlineMocksRule = new InlineMocksRule();

    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Test
    public void testFinalClass() {
        // Given
        String testName = "Ainz Ooal Gown";
        Client client = Mockito.mock(Client.class);
        Mockito.when(client.getName()).thenReturn(testName);

        // When
        String name = Utility.getName(client).orElseThrow();

        // Then
        assertEquals(testName, name);
    }

    static final class Client {
        final String getName() {
            return "text";
        }
    }

    static final class Utility {
        static Optional<String> getName(Client client) {
            return Optional.ofNullable(client).map(Client::getName);
        }
    }    
}

With a separate rule for inline mocks:

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.mockito.internal.configuration.plugins.Plugins;
import org.mockito.internal.util.MockUtil;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class InlineMocksRule implements TestRule {
    private static Field MOCK_MAKER_FIELD;

    static {
        try {
            MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup());
            VarHandle modifiers = lookup.findVarHandle(Field.class, "modifiers", int.class);

            MOCK_MAKER_FIELD = MockUtil.class.getDeclaredField("mockMaker");
            MOCK_MAKER_FIELD.setAccessible(true);

            int mods = MOCK_MAKER_FIELD.getModifiers();
            if (Modifier.isFinal(mods)) {
                modifiers.set(MOCK_MAKER_FIELD, mods & ~Modifier.FINAL);
            }
        } catch (IllegalAccessException | NoSuchFieldException ex) {
            throw new RuntimeException(ex);
        }
    }

    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                Object oldMaker = MOCK_MAKER_FIELD.get(null);
                MOCK_MAKER_FIELD.set(null, Plugins.getPlugins().getInlineMockMaker());
                try {
                    base.evaluate();
                } finally {
                    MOCK_MAKER_FIELD.set(null, oldMaker);
                }
            }
        };
    }
}
ursa
  • 4,404
  • 1
  • 24
  • 38
  • Thanks for your answer (+1). I found a way to fix it (answer to the second question). However the reason why the tests fail inside IntelliJ is still not clear to me. Furthermore, I can't reproduce it anymore (the failure inside the IntelliJ), which is totally weird. – Christos Dec 08 '19 at 12:47
1

Make sure you run the test with the same arguments. Check if your intellij run configurations match the jenkins. https://www.jetbrains.com/help/idea/creating-and-editing-run-debug-configurations.html. You can try to run test on local machine with the same arguments as on jenkins(from terminal), if it will fail that means the problem is in arguments

Link182
  • 733
  • 6
  • 15
  • The file `org.mockito.plugins.MockMaker` does exist also in jenkins machine. I do use the same JVM in bot machines. I will check the 3 you pointed out. Thanks – Christos Dec 05 '19 at 11:41
  • I tried to run the test through console, using the command used in Jenkins. They fail with the same exact error message. So something weird happens inside the IntelliJ. – Christos Dec 06 '19 at 10:38
  • Take a look to .idea/workspace.xml at your run configration, it is inside a tag. After that you can learn how to transform that xml into bash command – Link182 Dec 06 '19 at 13:45
  • Can you show jenkins terminal command which is used to run tests? Also can you tell me which package manager do you use? – Link182 Dec 06 '19 at 13:48
  • As a build tool, I use Gradle. – Christos Dec 06 '19 at 13:58
  • try to run the tests with gradle, example https://stackoverflow.com/a/39305180/6193843 with gradle wrapper the command should be ./gradlew test – Link182 Dec 06 '19 at 14:13
  • It failed again. Nothing changed. – Christos Dec 06 '19 at 15:01
  • Is hard to predict what is wrong, show me your configs from build.gradle. – Link182 Dec 06 '19 at 16:18
  • The problem is definitely inside IntelliJ. I say so, since the tests fail, if I run them locally in a terminal, using the same command that is used by jenkins. So I have to figure out what's wrong with IntelliJ. To make things worse, I noticed that sometimes a few 1 or 2 -after changes in build.gradle and after running the tests in terminal-, the tests were failing also in IntelliJ. As I have already mentioned, this happened only twice. This also reinforces my argument, about IntelliJ. – Christos Dec 06 '19 at 17:43
  • Thanks for your answer (+1). I found a way to fix it (answer to the second question). However the reason why the tests fail inside IntelliJ is still not clear to me. Furthermore, I can't reproduce it anymore (the failure inside the IntelliJ), which is totally weird. – Christos Dec 08 '19 at 12:48