While writing tests for @EnabledOnNative
and @DisabledOnNative
in Spring Native (https://github.com/spring-projects-experimental/spring-native/pull/1460), I encountered it is very difficult to mock the native detection result - NativeDetector.inNativeImage()
.
This is because it performs a system property check at the class initialization and keeps the result in a static final variable.
In this PR, I changed the detection logic to lookup SpringProperties
, similar to AotModeDetector
does in Spring Native.
This way, tests that need to modify the native-detection-result can interact with SpringProperties
to dynamically change the value.
Comment From: sbrannen
Hi @ttddyy,
Thanks for the proposal.
Since the system property in question is not one owned by the Spring Framework, we do not feel comfortable looking up its value via SpringProperties
.
Instead, we suggest you find an alternative approach to testing this.
For example, the following test class uses a custom ClassLoader
that allows you to set the org.graalvm.nativeimage.imagecode
before the NativeDetector
class is initialized.
import java.lang.reflect.Method;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link NativeDetector}.
*
* @author Sam Brannen
* @author Tadaya Tsuyukubo
*/
class NativeDetectorTests {
private static final String CLASS_NAME = "org.springframework.core.NativeDetector";
@Test
void inNativeImage() throws Exception {
assertThat(NativeDetector.inNativeImage()).isFalse();
try {
System.setProperty("org.graalvm.nativeimage.imagecode", "test");
Class<?> clazz = new TestClassLoader(getClass().getClassLoader()).loadClass(CLASS_NAME, false);
Method method = clazz.getMethod("inNativeImage");
Object result = ReflectionUtils.invokeMethod(method, null);
assertThat(result).asInstanceOf(InstanceOfAssertFactories.BOOLEAN).isTrue();
}
finally {
System.clearProperty("org.graalvm.nativeimage.imagecode");
}
assertThat(NativeDetector.inNativeImage()).isFalse();
}
private static class TestClassLoader extends OverridingClassLoader {
TestClassLoader(ClassLoader parent) {
super(parent);
}
@Override
protected boolean isEligibleForOverriding(String className) {
return CLASS_NAME.equals(className);
}
}
}
Hopefully you can use a similar approach in your tests for https://github.com/spring-projects-experimental/spring-native/pull/1460.
Please let us know if that works for you.
Comment From: ttddyy
@sbrannen Thanks for the suggestion. Using a throwaway classloader is such a neat technique.
It works for testing NativeDetector.inNativeImage()
method itself.
However, in my case, the test does not directly call the NativeDetector.inNativeImage()
.
The test target logic(junit extension classes launched programmatically) calls NativeDetector.inNativeImage()
.
So, I cannot directly interact with the throwaway classloader to mock the response.
Comment From: sbrannen
Hi @ttddyy,
That example was intended to be a proof of concept.
Here's a fully working test class for your DisabledOnNativeCondition
.
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.util.Optional;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.STRING;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link DisabledOnNativeCondition}.
*
* @author Sam Brannen
*/
class DisabledOnNativeConditionTests {
private static final String IMAGECODE_PROPERTY_NAME = "org.graalvm.nativeimage.imagecode";
private static final String NATIVE_DETECTOR_CLASS_NAME = "org.springframework.core.NativeDetector";
private static final String DISABLED_ON_NATIVE_CONDITION_CLASS_NAME = "org.springframework.core.DisabledOnNativeCondition";
@BeforeEach
void preconditions() {
assertThat(NativeDetector.inNativeImage()).isFalse();
}
@AfterEach
void postconditions() {
System.clearProperty(IMAGECODE_PROPERTY_NAME);
assertThat(NativeDetector.inNativeImage()).isFalse();
}
@Test
void enabledOnNativeImageWhenImageCodePropertyIsNotSet() throws Exception {
System.clearProperty(IMAGECODE_PROPERTY_NAME);
ConditionEvaluationResult result = getConditionEvaluationResult();
assertThat(result).isNotNull();
assertThat(result.isDisabled()).isFalse();
assertThat(result.getReason()).get().asInstanceOf(STRING).matches("^.+ is enabled on non native image$");
}
@Test
void disabledOnNativeImageWhenImageCodePropertyIsSet() throws Exception {
System.setProperty(IMAGECODE_PROPERTY_NAME, "test");
ConditionEvaluationResult result = getConditionEvaluationResult();
assertThat(result).isNotNull();
assertThat(result.isDisabled()).isTrue();
assertThat(result.getReason()).get().asInstanceOf(STRING).matches("^.+ is disabled on native image$");
}
private ConditionEvaluationResult getConditionEvaluationResult() throws Exception {
TestClassLoader testClassLoader = new TestClassLoader(getClass().getClassLoader());
Class<?> disbledOnNativeConditionClass = testClassLoader.loadClass(DISABLED_ON_NATIVE_CONDITION_CLASS_NAME, false);
Constructor<?> constructor = disbledOnNativeConditionClass.getDeclaredConstructor();
constructor.setAccessible(true);
ExecutionCondition condition = (ExecutionCondition) constructor.newInstance();
ExtensionContext context = mock(ExtensionContext.class);
when(context.getElement()).thenReturn(Optional.of(mock(AnnotatedElement.class)));
return condition.evaluateExecutionCondition(context);
}
private static class TestClassLoader extends OverridingClassLoader {
TestClassLoader(ClassLoader parent) {
super(parent);
}
@Override
protected boolean isEligibleForOverriding(String className) {
return NATIVE_DETECTOR_CLASS_NAME.equals(className) ||
DISABLED_ON_NATIVE_CONDITION_CLASS_NAME.equals(className);
}
}
}
Feel free to use that (or something based on that) in your PR for Spring Native.
Comment From: ttddyy
@sbrannen thanks for your time putting on this.
Initially, I was thinking to test the @EnabledOnNative
annotation itself by annotating classes and methods and launching them programmatically. So, it was more like an integration test rather than testing the condition implementation. But since it is a unit test, I believe testing the condition should be sufficient for the native check annotations. I'll apply the provided solution.
Comment From: sbrannen
Hi @ttddyy,
I'm glad that works for you.
However, after rethinking it, I think the approach I proposed is unnecessarily complex for the task at hand. A unit test certainly suffices, and we can achieve that without jumping through hoops using a custom ClassLoader
and reflection.
I'm not sure why I didn't immediately think of this, but... my usual approach for testing something that depends on something else that you cannot readily mock is to introduce a package-private (or protected) method which interacts with that "unmockable" thing (such as the OS environment or a static utility like NativeDetector
). Then you can mock the package-private method (or rather override it).
For example, that's how I test the @EnabledIfEnvironmentVariable
support in JUnit Jupiter.
https://github.com/junit-team/junit5/blob/89f6bbce2041ef29580df59ee86a9471a7698f3b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledIfEnvironmentVariableCondition.java#L63-L72
https://github.com/junit-team/junit5/blob/89f6bbce2041ef29580df59ee86a9471a7698f3b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/condition/EnabledIfEnvironmentVariableConditionTests.java#L34-L43
In your case, I would introduce the following method in your conditions and override it in your tests.
/**
* Determine if we are running in a native image.
*
* <p>The default implementation delegates to
* {@link NativeDetector#inNativeImage()}. Can be overridden in a subclass for
* testing purposes.
*/
boolean inNativeImage() {
return NativeDetector.inNativeImage();
}
Comment From: ttddyy
@sbrannen Yes, I agree with the suggestion. I also take that approach for my usual tests - slightly modify the main code for tests. Thanks for the advice. I have updated my code on the native side accordingly.