-
Notifications
You must be signed in to change notification settings - Fork 14k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
KAFKA-14133: Replace EasyMock with Mockito in streams tests #12607
Conversation
There were stubbings which were no longer on the call path so I have removed them. The way I checked that I wasn't changing the test behaviour was to use EasyMock.verify on the mocks and confirming that the stubbings were indeed unused prior to my change. There are multiple possibilities for refactoring, but I chose to keep the changes as close to the EasyMock implementation as the PR is already big. |
I am aware that there are merge conflicts and I will aim to address them over the coming days. |
@guozhangwang and @cadonna for visibility |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @clolov , just a few quick meta thoughts:
-
I think we can remove the
StateMachineTask
class as well since it was used as a mock really to compensate the incapabilities of easymock. While now with Mockito I think we would no longer need it. The only thing I'd need to ask is how to stubbing those state manipulations (e.g. ifsetCommitNeeded()
is called, then followedcommitNeeded
should return true). -
There are a few places where we used
expect.andReturn
which was for both stubbing the return values but as well as expecting those functions to be called, when they are replaced withwhen.thenReturn
we would not do verifications afaik, is that right? If yes could we fix them?
cc @cadonna for a second pass.
verify(task01Converted).initializeIfNeeded(); | ||
verify(stateUpdater).add(task00Converted); | ||
verify(stateUpdater).add(task01Converted); | ||
verify(activeTaskCreator).closeAndRemoveTaskProducerIfNeeded(any()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How to specify that we expect this function call only once? Should we use verify(activeTaskCreator, times(1)).func();
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto elsewhere for replacing expectLastCall().once()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By default it expects the method call to happen only once:
public static <T> T verify(T mock)
Verifies certain behavior happened once.
https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#verify(T)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack, thanks!
makeTaskFolders(taskId00.toString(), taskId01.toString()); | ||
expectLockObtainedFor(taskId00, taskId01); | ||
|
||
// The second attempt will return empty tasks. | ||
makeTaskFolders(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why we can remove those calls?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have altered the makeTaskFolders function to take the second call into account.
Before:
expect(stateDirectory.listNonEmptyTaskDirectories()).andReturn(taskFolders).once();
After:
when(stateDirectory.listNonEmptyTaskDirectories()).thenReturn(taskFolders).thenReturn(Collections.emptyList());
As far as I understand subsequent calls for stubbing in Mockito overwrite previous ones, so I cannot do exactly what was done with EasyMock.
@@ -4091,21 +3815,20 @@ public void shouldListNotPausedTasks() { | |||
topologyMetadata.pauseTopology(UNNAMED_TOPOLOGY); | |||
|
|||
assertEquals(taskManager.notPausedTasks().size(), 0); | |||
|
|||
verifyConsumerResumedWithAssignment(consumer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why add this additional verification?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The below method set an expectation for a method call which was not stubbed:
private static void expectRestoreToBeCompleted(final Consumer<byte[], byte[]> consumer,
final ChangelogReader changeLogReader,
final boolean changeLogUpdateRequired) {
final Set<TopicPartition> assignment = singleton(new TopicPartition("assignment", 0));
expect(consumer.assignment()).andReturn(assignment);
consumer.resume(assignment); <--- THIS IS THE EXPECTATION
expectLastCall();
expect(changeLogReader.completedChangelogs()).andReturn(emptySet()).times(changeLogUpdateRequired ? 1 : 0, 1);
}
I wanted to keep this verification so I moved it to a separate function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see, thanks!
verify(stateUpdater).remove(restoringActiveTaskToRecycle.id()); | ||
verify(stateUpdater).remove(restoringActiveTaskToClose.id()); | ||
|
||
verifyConsumerResumedWithAssignment(consumer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are several tests where we are adding this verification, could you elaborate a bit why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.
producer.commitTransaction(expectedCommittedOffsets, groupMetadata); | ||
expectLastCall(); | ||
|
||
task00.committedOffsets(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why we can remove those verifications now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I am aware Mockito cannot verify interactions with things which are not mocks. This being said, given you suggested I remove the StateMachineTask this might change in subsequent commits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds great, thanks.
expect(standbyTaskCreator.createTasks(eq(Collections.emptyMap()))).andReturn(Collections.emptySet()); | ||
|
||
replay(standbyTaskCreator, activeTaskCreator, consumer); | ||
when(activeTaskCreator.createTasks(any(), eq(Collections.emptyMap()))).thenReturn(Collections.emptySet()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here the goals are both verifying those functions should be called, and also return the results when they are called (you can see we did not use andStubReturn
here). When replacing them with when
we would not verify anymore right? Ditto elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we use @RunWith(MockitoJUnitRunner.StrictStubs.class)
and we run the whole test class Mockito verifies that all stubbings have indeed been used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack.
Hey @guozhangwang and thank you for the insightful comments! I will aim to give explanations and amend the PR in the upcoming days. |
b62306f
to
aad7ae5
Compare
Polite bump for a review here @cadonna. I have rebased on the latest trunk and I believe I have answered the majority of the comments already on the pull request. Given the size of the change I would prefer to get rid of StateMachineTask in a seprate pull request rather than as part of this one. |
aad7ae5
to
b19d229
Compare
b19d229
to
24f9db0
Compare
@cadonna I have rebased this on the latest trunk. Is it possible to get a review? This pull requests is one of the last remaining ones for completing the Mockito migration for streams. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@clolov Thanks for your patience!
Here my feedback for the the first 1500 lines. I will continue to review the PR in 1.5 weeks after my vacation.
.inState(State.RESTORING) | ||
.withInputPartitions(taskId00Partitions).build(); | ||
.inState(State.RESTORING) | ||
.withInputPartitions(taskId00Partitions).build(); | ||
final StandbyTask taskToRecycle1 = standbyTask(taskId01, taskId01ChangelogPartitions) | ||
.inState(State.RUNNING) | ||
.withInputPartitions(taskId01Partitions).build(); | ||
.inState(State.RUNNING) | ||
.withInputPartitions(taskId01Partitions).build(); | ||
final StandbyTask convertedTask0 = standbyTask(taskId00, taskId00ChangelogPartitions).build(); | ||
final StreamTask convertedTask1 = statefulTask(taskId01, taskId01ChangelogPartitions).build(); | ||
final StreamTask taskToClose = statefulTask(taskId02, taskId02ChangelogPartitions) | ||
.inState(State.RESTORING) | ||
.withInputPartitions(taskId02Partitions).build(); | ||
.inState(State.RESTORING) | ||
.withInputPartitions(taskId02Partitions).build(); | ||
final StreamTask taskToUpdateInputPartitions = statefulTask(taskId03, taskId03ChangelogPartitions) | ||
.inState(State.RESTORING) | ||
.withInputPartitions(taskId03Partitions).build(); | ||
.inState(State.RESTORING) | ||
.withInputPartitions(taskId03Partitions).build(); | ||
when(stateUpdater.hasRemovedTasks()).thenReturn(true); | ||
when(stateUpdater.drainRemovedTasks()) | ||
.thenReturn(mkSet(taskToRecycle0, taskToRecycle1, taskToClose, taskToUpdateInputPartitions)); | ||
.thenReturn(mkSet(taskToRecycle0, taskToRecycle1, taskToClose, taskToUpdateInputPartitions)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: adding the indentation is not necessary.
argThat(taskId -> !taskId.equals(taskToRecycle0.id()) && !taskId.equals(taskToRecycle1.id()))) | ||
argThat(taskId -> !taskId.equals(taskToRecycle0.id()) && !taskId.equals(taskToRecycle1.id()))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: adding indentation not necessary
when(activeTaskCreator.createActiveTaskFromStandby(taskToRecycle1, taskId01Partitions, consumer)) | ||
.thenReturn(convertedTask1); | ||
when(standbyTaskCreator.createStandbyTaskFromActive(taskToRecycle0, taskId00Partitions)) | ||
.thenReturn(convertedTask0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we usually indent 4 spaces.
@@ -1060,14 +996,12 @@ public void shouldTransitRestoredTaskToRunning() { | |||
final TasksRegistry tasks = mock(TasksRegistry.class); | |||
final TaskManager taskManager = setUpTransitionToRunningOfRestoredTask(task, tasks); | |||
consumer.resume(task.inputPartitions()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to become a verification
verify(consumer).resume(task.inputPartitions());
further down in the verification block.
expect(standbyTaskCreator.createStandbyTaskFromActive(statefulTask, statefulTask.inputPartitions())) | ||
.andStubReturn(standbyTask); | ||
when(standbyTaskCreator.createStandbyTaskFromActive(statefulTask, statefulTask.inputPartitions())) | ||
.thenReturn(standbyTask); | ||
activeTaskCreator.closeAndRemoveTaskProducerIfNeeded(statefulTask.id()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to become verification
verify(activeTaskCreator).closeAndRemoveTaskProducerIfNeeded(statefulTask.id());
in the verification block further down.
@@ -1268,16 +1188,14 @@ public void shouldCloseDirtyRestoredTask() { | |||
when(stateUpdater.restoresActiveTasks()).thenReturn(true); | |||
activeTaskCreator.closeAndRemoveTaskProducerIfNeeded(statefulTask.id()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to become verification
verify(activeTaskCreator).closeAndRemoveTaskProducerIfNeeded(statefulTask.id());
in the verification block.
@@ -1292,15 +1210,13 @@ public void shouldUpdateInputPartitionsOfRestoredTask() { | |||
when(stateUpdater.restoresActiveTasks()).thenReturn(true); | |||
final TaskManager taskManager = setUpTaskManager(ProcessingMode.AT_LEAST_ONCE, tasks, true); | |||
consumer.resume(statefulTask.inputPartitions()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to become verification
verify(consumer).resume(statefulTask.inputPartitions());
in the verification block.
private static void expectConsumerAssignmentPaused(final Consumer<byte[], byte[]> consumer) { | ||
final Set<TopicPartition> assignment = singleton(new TopicPartition("assignment", 0)); | ||
expect(consumer.assignment()).andReturn(assignment); | ||
consumer.pause(assignment); | ||
when(consumer.assignment()).thenReturn(assignment); | ||
doNothing().when(consumer).pause(assignment); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is not needed anymore.
verify(consumer).pause(assignment); | ||
verify(stateDirectory).unlock(taskId02); | ||
|
||
verifyConsumerResumedWithAssignment(consumer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method should also take the assignment as an argument. Maybe it is even better to call
verify(consumer, atLeastOnce()).resume(assignment);
instead of verifyConsumerResumedWithAssignment(consumer)
here.
@@ -1667,6 +1566,8 @@ public void shouldComputeOffsetSumForStandbyTask() throws Exception { | |||
restoringTask.setChangelogOffsets(changelogOffsets); | |||
|
|||
assertThat(taskManager.getTaskOffsetSums(), is(expectedOffsetSums)); | |||
|
|||
verifyConsumerResumedWithAssignment(consumer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you add this verification? It was not there before the migration, right?
@clolov Since this PR is rather long and hard to review, I took a stab to subdivide it into smaller PRs by migrating single mocks. I opened a PR for the migration of the topology builder mock: #13529. |
Heya @cadonna, thank you for the review! The approach you have taken in the other PR makes sense to me, so I will aim to open a few pull requests in a similar fashion. What worries you about the testing strategy in the class? |
My worry is that we verify interactions with mocks (mainly strict mocks) in unit tests in which we test unrelated aspects. That makes the test code more complicated than necessary. One example, is this from the other PR I linked above. In other words, we should probably try not only to migrate to Mockito but also to refactor a bit the unit tests where it makes sense to decrease the verifications on the mocks and thus the complexity of the tests. However, we should try to keep the refactorings at a minimum. |
Okay, this makes sense to me. I will aim to start opening PRs in the same manner as yours in the upcoming days and let's see where we go! |
I will close this PR as we have taken a different approach. Please refer to PRs linking to this one in order to find out how we have implemented the changes. |
Batch 5 of the tests detailed in https://issues.apache.org/jira/browse/KAFKA-14133 which use EasyMock and need to be moved to Mockito.