I'm working on an app that uses Java 12, Spring Boot 2.1.4.RELEASE and Mockito 2.23.4, and I'm trying to upgrade it to Java 15, Spring Boot 2.4.5 and Mockito 3.6.28 (provided in spring-boot-dependencies).
The problem I'm currently facing is with the following test:
private AbstractDrainService firstDrainService = mock(AbstractDrainService.class);
private AbstractDrainService secondDrainService = mock(AbstractDrainService.class);
private DrainManager drainManager;
@Before
public void setUp() throws Exception {
when(firstDrainService.hasPendingItems()).thenReturn(true, false);
when(secondDrainService.hasPendingItems()).thenReturn(true, false);
drainManager = new DrainManager();
drainManager.registerDrainService(firstDrainService, DrainManager.Priority.HIGHEST);
drainManager.registerDrainService(secondDrainService, DrainManager.Priority.HIGH);
drainManager.start();
}
public void test_drain_service_priority_order_is_respected_for_retries() throws Exception {
when(firstDrainService.hasPendingRetries()).thenReturn(true, false);
when(secondDrainService.hasPendingRetries()).thenReturn(true, false);
InOrder inOrder = inOrder(firstDrainService, secondDrainService);
inOrder.verify(firstDrainService).retry();
inOrder.verify(secondDrainService).retry();
}
It fails occasionally and when it does, Mockito throws a CannotStubVoidMethodWithReturnValue exception for a method prepare from AbstractDrainService that is never called in the test:
org.mockito.exceptions.misusing.CannotStubVoidMethodWithReturnValue:
'prepare' is a *void method* and it *cannot* be stubbed with a *return value*!
Voids are usually stubbed with Throwables:
doThrow(exception).when(mock).someVoidMethod();
If you need to set the void method to do nothing you can use:
doNothing().when(mock).someVoidMethod();
For more information, check out the javadocs for Mockito.doNothing().
***
If you're unsure why you're getting above error read on.
Due to the nature of the syntax above problem might occur because:
1. The method you are trying to stub is *overloaded*. Make sure you are calling the right overloaded version.
2. Somewhere in your test you are stubbing *final methods*. Sorry, Mockito does not verify/stub final methods.
3. A spy is stubbed using when(spy.foo()).then() syntax. It is safer to stub spies -
- with doReturn|Throw() family of methods. More in javadocs for Mockito.spy() method.
4. Mocking methods declared on non-public parent classes is not supported.
I also tried with doReturn(true, false).when(firstDrainService).retry(); and got the same result.
Implementations of AbstractDrainService
/**
* Wraps common drain functionality with constrained retry ability.
*/
public abstract class AbstractDrainService<T> {
/**
* Swaps read and write queues if read queue is empty and write queue is not.
*/
public synchronized void prepare() {}
/**
* Sends items to {@link #doDrain(List)} in {@link #drainSize} sized chunks for processing, until the queue is empty.
* In case of exception, item batch is added to retry list and processed on {@link #retry()} call,
* unless {@link #drainRetryThreshold} is set to 0 in which case failed batches are ignored and not retried
*/
public synchronized void drain() {}
/**
* Retries drain for each previously failed batch until drain succeeds or until {@link #drainRetryThreshold} is reached
*/
public synchronized void retry() {}
/**
* @return {@code true} if there are pending items or {@code false} if the retry queue is empty
*/
public boolean hasPendingRetries() {}
}
and DrainManager
/**
* Controls invocations of {@link AbstractDrainService#drain()} and {@link AbstractDrainService#retry()} on all registered drain services.
* Registered drain services are invoked once every second and in case of errors are retried once every two seconds.
* Services are executed in order dictated by their priority assigned through {@link DrainManager#registerDrainService(AbstractDrainService, int)}.
*/
@Component
public class DrainManager {
private ScheduledExecutorService executorService = null;
private LoggingProperties loggingProperties;
@PostConstruct
public void start() {
executorService = Executors.newScheduledThreadPool(2, new ThreadFactoryBuilder().setNameFormat("drain-manager-%d").build());
executorService.scheduleWithFixedDelay(this::drainAll, 0, loggingProperties.getDrain().getPeriod(), TimeUnit.MILLISECONDS);
executorService.scheduleWithFixedDelay(this::retryAll, 0, loggingProperties.getRetry().getPeriod(), TimeUnit.MILLISECONDS);
}
/**
* Registers the given drain service with the given priority.
*/
public void registerDrainService(AbstractDrainService drainService, int priority) {}
/**
* Invokes {@link AbstractDrainService#prepare()} on all registered drain services.
*/
private void prepareAll() {}
/**
* Invokes {@link AbstractDrainService#drain()} on all registered drain services
* for which {@link AbstractDrainService#hasPendingItems()} returns true.
*/
public void drainAll() {}
/**
* Invokes {@link AbstractDrainService#retry()} on all registered drain services
* for which {@link AbstractDrainService#hasPendingRetries()} returns true.
*/
public void retryAll() {}
}