Current Spring Boot version: 3.4.2 TestNG version: 7.10.2 Spring test: 6.2.2 (from parent spring depedency)
After Spring Boot 3.2.0, we were getting warning that @MockBean and @SpyBean were deprecated, but we continued using them. Since Spring Boot 3.4.0 they are marked for the removal, thus we migrated them to the @MockitoBean and @MockitySpyBean accordingly. On local, everything is working, they are running, but on the Jenkins we see an error that the object passed to when() is null. I tried many solutions, no luck, here is a scenario of usage (simplified). To keep the project's NDA I refactored code, but kept the main purpose of usage:
Hierarchy: MyTest <- SomeBaseAPITest <- BaseAPITest (with custom @DefaultTestConfig annotation) <- AbstractTestNGSpringContextTests
AbstractTestNGSpringContextTests is the class from spring
Logic: BaseAPITest is responsible for context SomeBaseAPITest is responsible for mocks creating MyTest is the child for the classes above
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootTest(classes = { Application.class },
properties = { "spring.main.allow-bean-definition-overriding=true" },
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ComponentScan({ "com.blabla.blabla" })
@TestConfiguration
public @interface DefaultTestConfig {
}
@DefaultTestConfig
public abstract class BaseAPITest extends AbstractTestNGSpringContextTests {
@LocalServerPort
private int port;
@BeforeSuite(alwaysRun = true)
public final void prepareTestContext() throws Exception {
log.info("Preparing test context");
super.springTestContextPrepareTestInstance();
}
}
@SuppressWarnings("unchecked")
public class SomeBaseAPITest extends BaseAPITest {
@MockitoSpyBean(reset = MockReset.NONE)
private SomeCsvServiceImpl someCsvService;
@BeforeSuite(alwaysRun = true, dependsOnMethods = "prepareTestContext")
protected void prepareMocks() {
doAnswer(invocationOnMock -> TestService.translate(invocationOnMock.getArgument(1)))
.when(morningstarCsvService)
.translate(any(), any(), any(), any(), any());
}
}
public class MyTest extends SomeBaseAPITest {
@Test
void myTest() {
var response = given()
.get(";);
assertTrue(response != null);
}
}
The error:
18:11:09 [INFO]
18:11:09 [INFO] Results:
18:11:09 [INFO]
18:11:09 [ERROR] Failures:
18:11:09 [ERROR] SomeBaseAPITest.prepareMocks:83 » NullInsteadOfMock
18:11:09 Argument passed to when() is null!
18:11:09 Example of correct stubbing:
18:11:09 doThrow(new RuntimeException()).when(mock).someMethod();
18:11:09 Also, if you use @Mock annotation don't miss openMocks()
18:11:09 [INFO]
18:11:09 [ERROR] Tests run: 357, Failures: 1, Errors: 0, Skipped: 356
The error means the code here:
doAnswer(invocationOnMock -> TestService.translate(invocationOnMock.getArgument(1)))
.when(morningstarCsvService)
.translate(any(), any(), any(), any(), any());
Why do we use such hierarchy of classes? To run the context once and reuse context in mocks configuration. Why two methods for beforeSuite? The one method loads the context (because the context is by default loaded in beforeClass, but in beforeSuite it is manually forced to load). The second one delegates creating mocks (in the real example there are more classes and more methods, here is an abstraction).
Again: it is working on local, and isn't on jenknins. Jenkins can see some error, and maybe it is correct. When hovering the mouse on the
@MockitoSpyBean(reset = MockReset.NONE)
private SomeCsvServiceImpl someCsvService;
it is showing, that someCsvService is not assigned (means the value is not initialized). When there was @MockBean annotation, there wasn't such warning. I hope to get help from the community!
Simplified Jenkins pipeline:
import hudson.util.Secret
node('maven38'){
try {
stage('Checkout code') {
git(branch: '$BRANCH', credentialsId: 'GITHUB', url: 'https://NDA')
}
stage('Run Test') {
withCredentials([usernamePassword(credentialsId: 'DEVOPS', passwordVariable: 'NEXUS_PASS', usernameVariable: 'NEXUS_USER')]) {
sh ''' mvn --settings ${WORKSPACE}/settings.xml -Dspring.profiles.active=test -Dsurefire.suiteXmlFiles=src/test/resources/suites/api-testng.xml clean test '''
}
}
} catch (e) {
echo 'This will run only if failed' throw e
} finally { ...
I tried changing private to protected, tried to move field to the test classes, tried to change the configuration with different combinations. I cannot migrate to the @MockBean to test as it is marked for removal (of course I can suppress removal warnings, but that's not a solution)
I expect to see no error with null instead of object in mock configuration on Jenkins.