From e9521abc6e0a924f761a6af04d1815c856774a06 Mon Sep 17 00:00:00 2001 From: NicklasWallgren Date: Sat, 22 Apr 2023 10:45:34 +0200 Subject: [PATCH] #patch: Enable snapshot-testing in a superclass Tests can now use `Expect` defined in a superclass --- .../snapshots/utils/ReflectionUtils.java | 48 +++++++++++++++++++ .../com/origin/snapshots/SnapshotHeaders.java | 2 +- .../junit4/SharedSnapshotHelpers.java | 11 +++-- .../com/origin/snapshots/BaseClassTest.java | 23 +++++++++ .../BaseClassTest$NestedClass.snap | 3 ++ .../snapshots/junit5/SnapshotExtension.java | 19 +++++--- .../com/origin/snapshots/BaseClassTest.java | 24 ++++++++++ .../BaseClassTest$NestedClass.snap | 3 ++ .../spock/SnapshotMethodInterceptor.groovy | 20 ++++---- .../origin/snapshots/SpecificationBase.groovy | 10 ++++ .../com/origin/snapshots/TestBaseSpec.groovy | 18 +++++++ .../snapshots/__snapshots__/TestBaseSpec.snap | 3 ++ 12 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/utils/ReflectionUtils.java create mode 100644 java-snapshot-testing-junit4/src/test/java/au/com/origin/snapshots/BaseClassTest.java create mode 100644 java-snapshot-testing-junit4/src/test/java/au/com/origin/snapshots/__snapshots__/BaseClassTest$NestedClass.snap create mode 100644 java-snapshot-testing-junit5/src/test/java/au/com/origin/snapshots/BaseClassTest.java create mode 100644 java-snapshot-testing-junit5/src/test/java/au/com/origin/snapshots/__snapshots__/BaseClassTest$NestedClass.snap create mode 100644 java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/SpecificationBase.groovy create mode 100644 java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/TestBaseSpec.groovy create mode 100644 java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/__snapshots__/TestBaseSpec.snap diff --git a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/utils/ReflectionUtils.java b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/utils/ReflectionUtils.java new file mode 100644 index 0000000..2f5b78b --- /dev/null +++ b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/utils/ReflectionUtils.java @@ -0,0 +1,48 @@ +package au.com.origin.snapshots.utils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Optional; +import java.util.function.Predicate; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class ReflectionUtils { + + /** + * Find {@link Field} by given predicate. + * + *

Invoke the given predicate on all fields in the target class, going up the class hierarchy + * to get all declared fields. + * + * @param clazz the target class to analyze + * @param predicate the predicate + * @return the field or empty optional + */ + public static Optional findFieldByPredicate( + final Class clazz, final Predicate predicate) { + Class targetClass = clazz; + + do { + final Field[] fields = targetClass.getDeclaredFields(); + for (final Field field : fields) { + if (!predicate.test(field)) { + continue; + } + return Optional.of(field); + } + targetClass = targetClass.getSuperclass(); + } while (targetClass != null && targetClass != Object.class); + + return Optional.empty(); + } + + public static void makeAccessible(final Field field) { + if ((!Modifier.isPublic(field.getModifiers()) + || !Modifier.isPublic(field.getDeclaringClass().getModifiers()) + || Modifier.isFinal(field.getModifiers())) + && !field.isAccessible()) { + field.setAccessible(true); + } + } +} diff --git a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/SnapshotHeaders.java b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/SnapshotHeaders.java index bb9ab8a..1f0b13b 100644 --- a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/SnapshotHeaders.java +++ b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/SnapshotHeaders.java @@ -55,5 +55,5 @@ public Snapshot apply(Object object, SnapshotSerializerContext snapshotSerialize snapshotSerializerContext.getHeader().put("custom2", "anything2"); return super.apply(object, snapshotSerializerContext); } - }; + } } diff --git a/java-snapshot-testing-junit4/src/main/java/au/com/origin/snapshots/junit4/SharedSnapshotHelpers.java b/java-snapshot-testing-junit4/src/main/java/au/com/origin/snapshots/junit4/SharedSnapshotHelpers.java index a223772..c6b01e4 100644 --- a/java-snapshot-testing-junit4/src/main/java/au/com/origin/snapshots/junit4/SharedSnapshotHelpers.java +++ b/java-snapshot-testing-junit4/src/main/java/au/com/origin/snapshots/junit4/SharedSnapshotHelpers.java @@ -4,6 +4,7 @@ import au.com.origin.snapshots.config.PropertyResolvingSnapshotConfig; import au.com.origin.snapshots.config.SnapshotConfig; import au.com.origin.snapshots.config.SnapshotConfigInjector; +import au.com.origin.snapshots.utils.ReflectionUtils; import java.lang.reflect.Method; import java.util.Arrays; import org.junit.runner.Description; @@ -14,13 +15,13 @@ class SharedSnapshotHelpers implements SnapshotConfigInjector { public void injectExpectInstanceVariable( SnapshotVerifier snapshotVerifier, Method testMethod, Object testInstance) { - Arrays.stream(testInstance.getClass().getDeclaredFields()) - .filter(it -> it.getType() == Expect.class) - .findFirst() + + ReflectionUtils.findFieldByPredicate( + testInstance.getClass(), (field) -> field.getType() == Expect.class) .ifPresent( - field -> { + (field) -> { Expect expect = Expect.of(snapshotVerifier, testMethod); - field.setAccessible(true); + ReflectionUtils.makeAccessible(field); try { field.set(testInstance, expect); } catch (IllegalAccessException e) { diff --git a/java-snapshot-testing-junit4/src/test/java/au/com/origin/snapshots/BaseClassTest.java b/java-snapshot-testing-junit4/src/test/java/au/com/origin/snapshots/BaseClassTest.java new file mode 100644 index 0000000..fbd78ad --- /dev/null +++ b/java-snapshot-testing-junit4/src/test/java/au/com/origin/snapshots/BaseClassTest.java @@ -0,0 +1,23 @@ +package au.com.origin.snapshots; + +import au.com.origin.snapshots.junit4.SnapshotRunner; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; + +@RunWith(Enclosed.class) +public class BaseClassTest { + + static class TestBase { + Expect expect; + } + + @RunWith(SnapshotRunner.class) + public static class NestedClass extends TestBase { + + @Test + public void helloWorldTest() { + expect.toMatchSnapshot("Hello World"); + } + } +} diff --git a/java-snapshot-testing-junit4/src/test/java/au/com/origin/snapshots/__snapshots__/BaseClassTest$NestedClass.snap b/java-snapshot-testing-junit4/src/test/java/au/com/origin/snapshots/__snapshots__/BaseClassTest$NestedClass.snap new file mode 100644 index 0000000..a659739 --- /dev/null +++ b/java-snapshot-testing-junit4/src/test/java/au/com/origin/snapshots/__snapshots__/BaseClassTest$NestedClass.snap @@ -0,0 +1,3 @@ +au.com.origin.snapshots.BaseClassTest$NestedClass.helloWorldTest=[ +Hello World +] \ No newline at end of file diff --git a/java-snapshot-testing-junit5/src/main/java/au/com/origin/snapshots/junit5/SnapshotExtension.java b/java-snapshot-testing-junit5/src/main/java/au/com/origin/snapshots/junit5/SnapshotExtension.java index aba8b32..ec21f86 100644 --- a/java-snapshot-testing-junit5/src/main/java/au/com/origin/snapshots/junit5/SnapshotExtension.java +++ b/java-snapshot-testing-junit5/src/main/java/au/com/origin/snapshots/junit5/SnapshotExtension.java @@ -7,10 +7,16 @@ import au.com.origin.snapshots.config.SnapshotConfigInjector; import au.com.origin.snapshots.exceptions.SnapshotMatchException; import au.com.origin.snapshots.logging.LoggingHelper; +import au.com.origin.snapshots.utils.ReflectionUtils; import java.lang.reflect.Field; -import java.util.Arrays; import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.extension.*; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor; import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; @@ -104,13 +110,12 @@ public Object resolveParameter( @Override public void beforeEach(ExtensionContext context) { if (context.getTestInstance().isPresent() && context.getTestMethod().isPresent()) { - Arrays.stream(context.getTestClass().get().getDeclaredFields()) - .filter(it -> it.getType() == Expect.class) - .findFirst() + ReflectionUtils.findFieldByPredicate( + context.getTestClass().get(), (field) -> field.getType() == Expect.class) .ifPresent( - field -> { + (field) -> { Expect expect = Expect.of(snapshotVerifier, context.getTestMethod().get()); - field.setAccessible(true); + ReflectionUtils.makeAccessible(field); try { field.set(context.getTestInstance().get(), expect); } catch (IllegalAccessException e) { diff --git a/java-snapshot-testing-junit5/src/test/java/au/com/origin/snapshots/BaseClassTest.java b/java-snapshot-testing-junit5/src/test/java/au/com/origin/snapshots/BaseClassTest.java new file mode 100644 index 0000000..8b89737 --- /dev/null +++ b/java-snapshot-testing-junit5/src/test/java/au/com/origin/snapshots/BaseClassTest.java @@ -0,0 +1,24 @@ +package au.com.origin.snapshots; + +import au.com.origin.snapshots.junit5.SnapshotExtension; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith({SnapshotExtension.class}) +public class BaseClassTest { + + class TestBase { + Expect expect; + } + + @Nested + @ExtendWith(SnapshotExtension.class) + class NestedClass extends TestBase { + + @Test + public void helloWorldTest() { + expect.toMatchSnapshot("Hello World"); + } + } +} diff --git a/java-snapshot-testing-junit5/src/test/java/au/com/origin/snapshots/__snapshots__/BaseClassTest$NestedClass.snap b/java-snapshot-testing-junit5/src/test/java/au/com/origin/snapshots/__snapshots__/BaseClassTest$NestedClass.snap new file mode 100644 index 0000000..a659739 --- /dev/null +++ b/java-snapshot-testing-junit5/src/test/java/au/com/origin/snapshots/__snapshots__/BaseClassTest$NestedClass.snap @@ -0,0 +1,3 @@ +au.com.origin.snapshots.BaseClassTest$NestedClass.helloWorldTest=[ +Hello World +] \ No newline at end of file diff --git a/java-snapshot-testing-spock/src/main/groovy/au/com/origin/snapshots/spock/SnapshotMethodInterceptor.groovy b/java-snapshot-testing-spock/src/main/groovy/au/com/origin/snapshots/spock/SnapshotMethodInterceptor.groovy index ea6239d..ac94958 100644 --- a/java-snapshot-testing-spock/src/main/groovy/au/com/origin/snapshots/spock/SnapshotMethodInterceptor.groovy +++ b/java-snapshot-testing-spock/src/main/groovy/au/com/origin/snapshots/spock/SnapshotMethodInterceptor.groovy @@ -1,12 +1,11 @@ package au.com.origin.snapshots.spock import au.com.origin.snapshots.Expect +import au.com.origin.snapshots.utils.ReflectionUtils import au.com.origin.snapshots.SnapshotVerifier import au.com.origin.snapshots.logging.LoggingHelper -import lombok.extern.slf4j.Slf4j import org.slf4j.LoggerFactory import org.spockframework.runtime.extension.AbstractMethodInterceptor -import org.spockframework.runtime.extension.IMethodInterceptor import org.spockframework.runtime.extension.IMethodInvocation import java.lang.reflect.Method @@ -40,13 +39,16 @@ class SnapshotMethodInterceptor extends AbstractMethodInterceptor { } private void updateInstanceVariable(Object testInstance, Method testMethod) { - testInstance.class.declaredFields - .find { it.getType() == Expect.class } - ?.with { - Expect expect = Expect.of(snapshotVerifier, testMethod) - it.setAccessible(true) - it.set(testInstance, expect) - } + ReflectionUtils.findFieldByPredicate(testInstance.class, { field -> field.getType() == Expect.class }) + .ifPresent({ field -> + Expect expect = Expect.of(snapshotVerifier, testMethod); + ReflectionUtils.makeAccessible(field); + try { + field.set(testInstance, expect); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }); } @Override diff --git a/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/SpecificationBase.groovy b/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/SpecificationBase.groovy new file mode 100644 index 0000000..c5f7389 --- /dev/null +++ b/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/SpecificationBase.groovy @@ -0,0 +1,10 @@ +package au.com.origin.snapshots + +import org.junit.runner.RunWith +import org.spockframework.runtime.Sputnik +import spock.lang.Specification + +@RunWith(Sputnik.class) +class SpecificationBase extends Specification { + Expect expect; +} diff --git a/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/TestBaseSpec.groovy b/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/TestBaseSpec.groovy new file mode 100644 index 0000000..9383a83 --- /dev/null +++ b/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/TestBaseSpec.groovy @@ -0,0 +1,18 @@ +package au.com.origin.snapshots + +import au.com.origin.snapshots.annotations.SnapshotName +import au.com.origin.snapshots.spock.EnableSnapshots + +@EnableSnapshots +class TestBaseSpec extends SpecificationBase { + + @SnapshotName("Should use extension") + def "Should use extension"() { + when: + expect.toMatchSnapshot("Hello World") + + then: + true + } + +} diff --git a/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/__snapshots__/TestBaseSpec.snap b/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/__snapshots__/TestBaseSpec.snap new file mode 100644 index 0000000..7cf9614 --- /dev/null +++ b/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/__snapshots__/TestBaseSpec.snap @@ -0,0 +1,3 @@ +Should use extension=[ +Hello World +] \ No newline at end of file