-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Introduce QuarkusTestProfileAwareClassOrderer for efficient @TestProfile
ordering
#20156
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
87 changes: 87 additions & 0 deletions
87
.../junit5/src/main/java/io/quarkus/test/junit/util/QuarkusTestProfileAwareClassOrderer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package io.quarkus.test.junit.util; | ||
|
||
import java.util.Comparator; | ||
import java.util.Optional; | ||
|
||
import org.junit.jupiter.api.ClassDescriptor; | ||
import org.junit.jupiter.api.ClassOrderer; | ||
import org.junit.jupiter.api.ClassOrdererContext; | ||
|
||
import io.quarkus.test.junit.QuarkusIntegrationTest; | ||
import io.quarkus.test.junit.QuarkusTest; | ||
import io.quarkus.test.junit.TestProfile; | ||
|
||
/** | ||
* A {@link ClassOrderer} that orders {@link QuarkusTest} and {@link QuarkusIntegrationTest} classes for minimum Quarkus | ||
* restarts by grouping them by their {@link TestProfile}. | ||
* <p/> | ||
* By default, Quarkus*Tests not using any profile come first, then classes using a profile (in groups) and then all other | ||
* non-Quarkus tests (e.g. plain unit tests). | ||
* <p/> | ||
* Internally, ordering is based on three prefixes that are prepended to the fully qualified name of the respective class, with | ||
* the fully qualified class name of the {@link io.quarkus.test.junit.QuarkusTestProfile QuarkusTestProfile} as an infix (if | ||
* present). | ||
* The default prefixes are defined by {@code DEFAULT_ORDER_PREFIX_*} and can be overridden in {@code junit-platform.properties} | ||
* via {@code CFGKEY_ORDER_PREFIX_*}, e.g. non-Quarkus tests can be run first (not last) by setting | ||
* {@link #CFGKEY_ORDER_PREFIX_NON_QUARKUS_TEST} to {@code 10_}. | ||
* <p/> | ||
* {@link #getCustomOrderKey(ClassDescriptor, ClassOrdererContext)} can be overridden to provide a custom order number for a | ||
* given test class, e.g. based on {@link org.junit.jupiter.api.Tag}, class name or something else. | ||
* <p/> | ||
* Limitations: | ||
* <ul> | ||
* <li>This orderer does not (yet) consider {@linkplain io.quarkus.test.common.QuarkusTestResource#restrictToAnnotatedClass() | ||
* test resources that are restricted to the annotated class}.</li> | ||
* <li>Only JUnit5 test classes are subject to ordering, e.g. ArchUnit test classes are not passed to this orderer.</li> | ||
* </ul> | ||
*/ | ||
public class QuarkusTestProfileAwareClassOrderer implements ClassOrderer { | ||
|
||
protected static final String DEFAULT_ORDER_PREFIX_QUARKUS_TEST = "20_"; | ||
protected static final String DEFAULT_ORDER_PREFIX_QUARKUS_TEST_WITH_PROFILE = "40_"; | ||
protected static final String DEFAULT_ORDER_PREFIX_NON_QUARKUS_TEST = "60_"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: The "number spacing" might seem odd, but I wanted to leave some gaps for users to fill in their own orderings. |
||
|
||
static final String CFGKEY_ORDER_PREFIX_QUARKUS_TEST = "quarkus.test.orderer.prefix.quarkus-test"; | ||
static final String CFGKEY_ORDER_PREFIX_QUARKUS_TEST_WITH_PROFILE = "quarkus.test.orderer.prefix.quarkus-test-with-profile"; | ||
static final String CFGKEY_ORDER_PREFIX_NON_QUARKUS_TEST = "quarkus.test.orderer.prefix.non-quarkus-test"; | ||
|
||
@Override | ||
public void orderClasses(ClassOrdererContext context) { | ||
if (context.getClassDescriptors().size() <= 1) { | ||
return; | ||
} | ||
var prefixQuarkusTest = context.getConfigurationParameter(CFGKEY_ORDER_PREFIX_QUARKUS_TEST) | ||
.orElse(DEFAULT_ORDER_PREFIX_QUARKUS_TEST); | ||
var prefixQuarkusTestWithProfile = context.getConfigurationParameter(CFGKEY_ORDER_PREFIX_QUARKUS_TEST_WITH_PROFILE) | ||
.orElse(DEFAULT_ORDER_PREFIX_QUARKUS_TEST_WITH_PROFILE); | ||
var prefixNonQuarkusTest = context.getConfigurationParameter(CFGKEY_ORDER_PREFIX_NON_QUARKUS_TEST) | ||
.orElse(DEFAULT_ORDER_PREFIX_NON_QUARKUS_TEST); | ||
|
||
context.getClassDescriptors().sort(Comparator.comparing(classDescriptor -> { | ||
Optional<String> customOrderKey = getCustomOrderKey(classDescriptor, context); | ||
if (customOrderKey.isPresent()) { | ||
return customOrderKey.get(); | ||
} | ||
var testClassName = classDescriptor.getTestClass().getName(); | ||
if (classDescriptor.isAnnotated(QuarkusTest.class) | ||
|| classDescriptor.isAnnotated(QuarkusIntegrationTest.class)) { | ||
return classDescriptor.findAnnotation(TestProfile.class) | ||
.map(TestProfile::value) | ||
.map(profileClass -> prefixQuarkusTestWithProfile + profileClass.getName() + "@" + testClassName) | ||
.orElseGet(() -> prefixQuarkusTest + testClassName); | ||
} | ||
return prefixNonQuarkusTest + testClassName; | ||
})); | ||
} | ||
|
||
/** | ||
* Template method that provides an optional custom order key for the given {@code classDescriptor}. | ||
* | ||
* @param classDescriptor the respective test class | ||
* @param context for config lookup | ||
* @return optional custom order key for the given test class | ||
*/ | ||
protected Optional<String> getCustomOrderKey(ClassDescriptor classDescriptor, ClassOrdererContext context) { | ||
return Optional.empty(); | ||
} | ||
} |
154 changes: 154 additions & 0 deletions
154
...it5/src/test/java/io/quarkus/test/junit/util/QuarkusTestProfileAwareClassOrdererTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
package io.quarkus.test.junit.util; | ||
|
||
import static io.quarkus.test.junit.util.QuarkusTestProfileAwareClassOrderer.CFGKEY_ORDER_PREFIX_NON_QUARKUS_TEST; | ||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.mockito.ArgumentMatchers.anyString; | ||
import static org.mockito.Mockito.doReturn; | ||
import static org.mockito.Mockito.never; | ||
import static org.mockito.Mockito.verify; | ||
import static org.mockito.Mockito.when; | ||
import static org.mockito.Mockito.withSettings; | ||
|
||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
import org.junit.jupiter.api.ClassDescriptor; | ||
import org.junit.jupiter.api.ClassOrdererContext; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.mockito.Mock; | ||
import org.mockito.Mockito; | ||
import org.mockito.junit.jupiter.MockitoExtension; | ||
|
||
import io.quarkus.test.junit.QuarkusTest; | ||
import io.quarkus.test.junit.QuarkusTestProfile; | ||
import io.quarkus.test.junit.TestProfile; | ||
|
||
@ExtendWith(MockitoExtension.class) | ||
class QuarkusTestProfileAwareClassOrdererTest { | ||
|
||
@Mock | ||
ClassOrdererContext contextMock; | ||
|
||
QuarkusTestProfileAwareClassOrderer underTest = new QuarkusTestProfileAwareClassOrderer(); | ||
|
||
@Test | ||
void singleClass() { | ||
doReturn(Arrays.asList(descriptorMock(Test1.class))) | ||
.when(contextMock).getClassDescriptors(); | ||
|
||
underTest.orderClasses(contextMock); | ||
|
||
verify(contextMock, never()).getConfigurationParameter(anyString()); | ||
} | ||
|
||
@Test | ||
void allVariants() { | ||
ClassDescriptor quarkusTest1Desc = quarkusDescriptorMock(Test1.class, null); | ||
ClassDescriptor quarkusTest2Desc = quarkusDescriptorMock(Test2.class, null); | ||
ClassDescriptor quarkusTestWithProfile1Desc = quarkusDescriptorMock(Test3.class, Profile1.class); | ||
ClassDescriptor quarkusTestWithProfile2Test4Desc = quarkusDescriptorMock(Test4.class, Profile2.class); | ||
ClassDescriptor quarkusTestWithProfile2Test5Desc = quarkusDescriptorMock(Test5.class, Profile2.class); | ||
ClassDescriptor nonQuarkusTest6Desc = descriptorMock(Test6.class); | ||
ClassDescriptor nonQuarkusTest7Desc = descriptorMock(Test7.class); | ||
List<ClassDescriptor> input = Arrays.asList( | ||
nonQuarkusTest7Desc, | ||
quarkusTestWithProfile2Test5Desc, | ||
quarkusTest2Desc, | ||
nonQuarkusTest6Desc, | ||
quarkusTest1Desc, | ||
quarkusTestWithProfile2Test4Desc, | ||
quarkusTestWithProfile1Desc); | ||
doReturn(input).when(contextMock).getClassDescriptors(); | ||
|
||
underTest.orderClasses(contextMock); | ||
|
||
assertThat(input).containsExactly( | ||
quarkusTest1Desc, | ||
quarkusTest2Desc, | ||
quarkusTestWithProfile1Desc, | ||
quarkusTestWithProfile2Test4Desc, | ||
quarkusTestWithProfile2Test5Desc, | ||
nonQuarkusTest6Desc, | ||
nonQuarkusTest7Desc); | ||
} | ||
|
||
@Test | ||
void configuredPrefix() { | ||
ClassDescriptor quarkusTestDesc = quarkusDescriptorMock(Test1.class, null); | ||
ClassDescriptor nonQuarkusTestDesc = descriptorMock(Test2.class); | ||
List<ClassDescriptor> input = Arrays.asList(quarkusTestDesc, nonQuarkusTestDesc); | ||
doReturn(input).when(contextMock).getClassDescriptors(); | ||
|
||
when(contextMock.getConfigurationParameter(anyString())).thenReturn(Optional.empty()); | ||
// prioritize unit tests | ||
when(contextMock.getConfigurationParameter(CFGKEY_ORDER_PREFIX_NON_QUARKUS_TEST)).thenReturn(Optional.of("01_")); | ||
|
||
underTest.orderClasses(contextMock); | ||
|
||
assertThat(input).containsExactly(nonQuarkusTestDesc, quarkusTestDesc); | ||
} | ||
|
||
@Test | ||
void customOrderKey() { | ||
ClassDescriptor quarkusTest1Desc = quarkusDescriptorMock(Test1.class, null); | ||
ClassDescriptor quarkusTest2Desc = quarkusDescriptorMock(Test2.class, null); | ||
List<ClassDescriptor> input = Arrays.asList(quarkusTest1Desc, quarkusTest2Desc); | ||
doReturn(input).when(contextMock).getClassDescriptors(); | ||
|
||
underTest = new QuarkusTestProfileAwareClassOrderer() { | ||
@Override | ||
protected Optional<String> getCustomOrderKey(ClassDescriptor classDescriptor, ClassOrdererContext context) { | ||
return classDescriptor == quarkusTest2Desc ? Optional.of("00_first") : Optional.empty(); | ||
} | ||
}; | ||
underTest.orderClasses(contextMock); | ||
|
||
assertThat(input).containsExactly(quarkusTest2Desc, quarkusTest1Desc); | ||
} | ||
|
||
private ClassDescriptor descriptorMock(Class<?> testClass) { | ||
ClassDescriptor mock = Mockito.mock(ClassDescriptor.class, withSettings().lenient().name(testClass.getSimpleName())); | ||
doReturn(testClass).when(mock).getTestClass(); | ||
return mock; | ||
} | ||
|
||
private ClassDescriptor quarkusDescriptorMock(Class<?> testClass, Class<? extends QuarkusTestProfile> profileClass) { | ||
ClassDescriptor mock = descriptorMock(testClass); | ||
when(mock.isAnnotated(QuarkusTest.class)).thenReturn(true); | ||
if (profileClass != null) { | ||
TestProfile profileMock = Mockito.mock(TestProfile.class); | ||
doReturn(profileClass).when(profileMock).value(); | ||
when(mock.findAnnotation(TestProfile.class)).thenReturn(Optional.of(profileMock)); | ||
} | ||
return mock; | ||
} | ||
|
||
private static class Test1 { | ||
}; | ||
|
||
private static class Test2 { | ||
}; | ||
|
||
private static class Test3 { | ||
}; | ||
|
||
private static class Test4 { | ||
}; | ||
|
||
private static class Test5 { | ||
}; | ||
|
||
private static class Test6 { | ||
}; | ||
|
||
private static class Test7 { | ||
}; | ||
|
||
private static class Profile1 implements QuarkusTestProfile { | ||
} | ||
|
||
private static class Profile2 implements QuarkusTestProfile { | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 could be a future improvement.
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.
FTR: #20420