Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mockito mocks locally final class but fails in Jenkins

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.

like image 409
Christos Avatar asked Nov 22 '19 10:11

Christos


People also ask

Why tests pass locally but fail in Jenkins?

Since the Jenkins machine/VM/pod/container is likely running different (slower) hardware than your local machine, tests dealing with multiple threads or inadvertent race conditions may run differently and fail in Jenkins.

Can Mockito mock a final class?

You cannot mock a final class with Mockito, as you can't do it by yourself.

Can we test final classes and primitive types using Mockito?

Configure Mockito for Final Methods and Classes Before we can use Mockito for mocking final classes and methods, we have to configure it. Mockito checks the extensions directory for configuration files when it is loaded. This file enables the mocking of final methods and classes.

Is Mockito better than EasyMock?

If we care about the maintainability of tests (easier to read, write and having less brittle tests which are not affected much by change), then Mockito seems a better choice. My questions are: If you have used EasyMock in large scale projects, do you find that your tests are harder to maintain?


2 Answers

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/

like image 83
Johnny Alpha Avatar answered Sep 23 '22 20:09

Johnny Alpha


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);
                }
            }
        };
    }
}
like image 44
ursa Avatar answered Sep 22 '22 20:09

ursa