Skip to content
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

Closed
wants to merge 2 commits into from

Conversation

clolov
Copy link
Contributor

@clolov clolov commented Sep 8, 2022

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.

@clolov
Copy link
Contributor Author

clolov commented Sep 8, 2022

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.

@clolov
Copy link
Contributor Author

clolov commented Sep 8, 2022

I am aware that there are merge conflicts and I will aim to address them over the coming days.

@clolov
Copy link
Contributor Author

clolov commented Sep 8, 2022

@guozhangwang and @cadonna for visibility

@cadonna cadonna added the streams label Sep 8, 2022
Copy link
Contributor

@guozhangwang guozhangwang left a 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:

  1. 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. if setCommitNeeded() is called, then followed commitNeeded should return true).

  2. 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 with when.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());
Copy link
Contributor

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?

Copy link
Contributor

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()?

Copy link
Contributor Author

@clolov clolov Sep 12, 2022

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)

Copy link
Contributor

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();
Copy link
Contributor

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?

Copy link
Contributor Author

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);
Copy link
Contributor

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?

Copy link
Contributor Author

@clolov clolov Sep 12, 2022

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.

Copy link
Contributor

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);
Copy link
Contributor

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?

Copy link
Contributor Author

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();
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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());
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack.

@clolov
Copy link
Contributor Author

clolov commented Sep 9, 2022

Hey @guozhangwang and thank you for the insightful comments! I will aim to give explanations and amend the PR in the upcoming days.

@clolov
Copy link
Contributor Author

clolov commented Nov 24, 2022

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.

@mjsax mjsax added the tests Test fixes (including flaky tests) label Dec 28, 2022
@clolov
Copy link
Contributor Author

clolov commented Feb 2, 2023

@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.

Copy link
Contributor

@cadonna cadonna left a 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.

Comment on lines -913 to +868
.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));
Copy link
Contributor

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.

Comment on lines -944 to +881
argThat(taskId -> !taskId.equals(taskToRecycle0.id()) && !taskId.equals(taskToRecycle1.id())))
argThat(taskId -> !taskId.equals(taskToRecycle0.id()) && !taskId.equals(taskToRecycle1.id())))
Copy link
Contributor

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

Comment on lines +870 to +873
when(activeTaskCreator.createActiveTaskFromStandby(taskToRecycle1, taskId01Partitions, consumer))
.thenReturn(convertedTask1);
when(standbyTaskCreator.createStandbyTaskFromActive(taskToRecycle0, taskId00Partitions))
.thenReturn(convertedTask0);
Copy link
Contributor

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());
Copy link
Contributor

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());
Copy link
Contributor

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());
Copy link
Contributor

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());
Copy link
Contributor

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.

Comment on lines 4099 to 4103
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);
}
Copy link
Contributor

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);
Copy link
Contributor

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);
Copy link
Contributor

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?

@cadonna
Copy link
Contributor

cadonna commented Apr 8, 2023

@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.
Let me know what you think about it.
If you think that is a good idea feel free to migrate the other mocks. I think we also need a bit to rethink the testing strategy in this test class. Sorry that this migration gets so complicated.

@clolov
Copy link
Contributor Author

clolov commented Apr 12, 2023

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?

@cadonna
Copy link
Contributor

cadonna commented Apr 13, 2023

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.

@clolov
Copy link
Contributor Author

clolov commented Apr 18, 2023

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!

@clolov
Copy link
Contributor Author

clolov commented Jun 27, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
streams tests Test fixes (including flaky tests)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants