From 39e59c7d6b3a4f59a5639f560fd1bc376bc77011 Mon Sep 17 00:00:00 2001 From: Vyacheslav Rusakov Date: Thu, 20 Feb 2025 15:37:41 +0700 Subject: [PATCH] add lambda-based listener methods for TestEnvironmentSetup (alternative to listen method); move helper test objects lookup methods from listener interface into EventContext object (passed into listener) for lambda listeners usage simplification --- CHANGELOG.md | 4 +- .../guice/test/jupiter/env/EnableSetup.java | 2 +- .../test/jupiter/env/ListenersSupport.java | 14 +- .../jupiter/env/TestEnvironmentSetup.java | 7 +- .../jupiter/env/TestExecutionListener.java | 179 ------------------ .../guice/test/jupiter/env/TestExtension.java | 114 +++++++++++ .../env/field/AnnotatedTestFieldSetup.java | 73 ++++--- .../test/jupiter/env/listen/EventContext.java | 75 ++++++++ .../env/listen/TestExecutionListener.java | 105 ++++++++++ .../env/listen/lambda/LambdaTestListener.java | 26 +++ .../env/listen/lambda/ListenerEvent.java | 12 ++ .../TestExecutionListenerLambdaAdapter.java | 66 +++++++ .../test/jupiter/ext/mock/MocksSupport.java | 18 +- .../test/jupiter/ext/spy/SpiesSupport.java | 21 +- .../test/jupiter/ext/stub/StubsSupport.java | 17 +- .../jupiter/ext/track/TrackersSupport.java | 26 ++- .../guice/test/util/TestSetupUtils.java | 2 +- .../jupiter/setup/ListenerLambdaTest.java | 156 +++++++++++++++ .../test/jupiter/setup/ListenerTest.java | 60 +++--- .../SetupObjectAutoRegistrationTest.java | 6 +- 20 files changed, 676 insertions(+), 307 deletions(-) delete mode 100644 dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestExecutionListener.java create mode 100644 dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/EventContext.java create mode 100644 dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/TestExecutionListener.java create mode 100644 dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/LambdaTestListener.java create mode 100644 dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/ListenerEvent.java create mode 100644 dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/TestExecutionListenerLambdaAdapter.java create mode 100644 dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ListenerLambdaTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e152240e..c7a90f79e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,8 @@ (useful when TestInstance.Lifecycle.PER_CLASS used) (discussion #394) - Add getJunitContext() method for TestEnvironmentSetup (@EnableSetup) to be able to configure test application with full context access (a setup object could register hooks) (discussion #388) - - Add test lifecycle listeners: could be registered with TestEnvironmentSetup (listen() method) and provide - notifications for guicey extension lifecycle (app start/stop, before/after test). + - Add test lifecycle listeners: could be registered with TestEnvironmentSetup (listen() method or lambda-based on* methods) + and provide notifications for guicey extension lifecycle (app start/stop, before/after test). This is a simple alternative to writing junit extensions for an additional integrations (db, testcontainers etc.). - Add annotated fields search api in test class for setup objects (TestEnvironmentSetup): findFields(..) (to simplify writing annotation-driven extensions). diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/EnableSetup.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/EnableSetup.java index d9f0279ed..c1f6f6a3f 100644 --- a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/EnableSetup.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/EnableSetup.java @@ -21,7 +21,7 @@ * method), otherwise it must be static. Incorrect usage will be indicated with an exception. *

* If setup object implements hook ({@link ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook}) - * and/or listener ({@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestExecutionListener}) it would be + * and/or listener ({@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener}) it would be * registered automatically (no need for manual registration). Manual registration would not create duplicate. * * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/ListenersSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/ListenersSupport.java index 5dba79777..2706438c8 100644 --- a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/ListenersSupport.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/ListenersSupport.java @@ -3,6 +3,8 @@ import com.google.common.base.Preconditions; import com.google.common.base.Stopwatch; import org.junit.jupiter.api.extension.ExtensionContext; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener; import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track.GuiceyTestTime; import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track.TestExtensionsTracker; @@ -30,27 +32,27 @@ public void addListener(final TestExecutionListener listener) { } public void broadcastStart(final ExtensionContext context) { - broadcast(listener -> listener.started(context), false); + broadcast(listener -> listener.started(new EventContext(context)), false); } public void broadcastBeforeAll(final ExtensionContext context) { - broadcast(listener -> listener.beforeAll(context), true); + broadcast(listener -> listener.beforeAll(new EventContext(context)), true); } public void broadcastBefore(final ExtensionContext context) { - broadcast(listener -> listener.beforeEach(context), true); + broadcast(listener -> listener.beforeEach(new EventContext(context)), true); } public void broadcastAfter(final ExtensionContext context) { - broadcast(listener -> listener.afterEach(context), true); + broadcast(listener -> listener.afterEach(new EventContext(context)), true); } public void broadcastAfterAll(final ExtensionContext context) { - broadcast(listener -> listener.afterAll(context), true); + broadcast(listener -> listener.afterAll(new EventContext(context)), true); } public void broadcastStop(final ExtensionContext context) { - broadcast(listener -> listener.stopped(context), false); + broadcast(listener -> listener.stopped(new EventContext(context)), false); } private void broadcast(final Consumer action, final boolean append) { diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestEnvironmentSetup.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestEnvironmentSetup.java index a2c2cc263..940d0f43c 100644 --- a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestEnvironmentSetup.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestEnvironmentSetup.java @@ -13,8 +13,9 @@ * closed automatically. *

* If auto close is not enough, use - * {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension#listen(TestExecutionListener)} listener for - * reacting on exact test phases. + * {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension#listen( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)} listener for reacting on exact test + * phases (or lambda-based listener methods: on*). *

* The same could be achieved with an additional junit 5 extensions, but it might be harder to properly synchronize * lifecycles (extensions order would be important). Environment support assumed to be a simpler alternative. @@ -31,7 +32,7 @@ * (inside it) providing entire junit context or just some stored values. *

* AUTO REGISTRATION: If setup object implements hook ({@link ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook}) - * and/or listener ({@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestExecutionListener}) it would be + * and/or listener ({@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener}) it would be * registered automatically. Manual registration would not create duplicate. * * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestExecutionListener.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestExecutionListener.java deleted file mode 100644 index 9a823274c..000000000 --- a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestExecutionListener.java +++ /dev/null @@ -1,179 +0,0 @@ -package ru.vyarus.dropwizard.guice.test.jupiter.env; - -import com.google.inject.Injector; -import io.dropwizard.testing.DropwizardTestSupport; -import org.junit.jupiter.api.extension.ExtensionContext; -import ru.vyarus.dropwizard.guice.test.ClientSupport; -import ru.vyarus.dropwizard.guice.test.jupiter.ext.GuiceyExtensionsSupport; - -/** - * Test listener allows listening for the main test events. There are no beforeAll and afterAll events directly - * because guicey extension could be created either on beforeAll or in beforeEach (depends on test configuration). - *

- * Before/after test called before and after each test method. This could be used for custom setup/cleanup logic. - * BeforeAll and afterAll might not be called - use with caution (depends on extension registration). - *

- * This interface provides a simple replacement for junit extension (synchronized with guicey lifecycle). - * - * @author Vyacheslav Rusakov - * @since 07.02.2025 - */ -public interface TestExecutionListener { - - /** - * Called when dropwizard (or guicey) application started. It could be beforeAll or beforeEach phase - * (if important, look {@link org.junit.jupiter.api.extension.ExtensionContext#getTestMethod()} to make sure). - * Application could start/stop multiple times within one test class (if extension registered in non-static field). - *

- * NOTE: At this stage, injections not yet performed inside test instance. - *

- * This method could be used instead of beforeAll because normally extension is created under beforeAll, but - * for extensions created under beforeEach - it would be impossible to notify about beforeAll anyway. - * - * @param context junit extension - * @see #getSupport(org.junit.jupiter.api.extension.ExtensionContext) - * @see #getInjector(org.junit.jupiter.api.extension.ExtensionContext) - * @see #getBean(org.junit.jupiter.api.extension.ExtensionContext, Class) - * @see #getClient(org.junit.jupiter.api.extension.ExtensionContext) - */ - default void started(final ExtensionContext context) { - // empty default - } - - /** - * IMPORTANT: this method MIGHT NOT BE CALLED at all in case if extension is registered under non-static field - * (and so application created before each method). - * Prefer {@link #started(org.junit.jupiter.api.extension.ExtensionContext)} instead, which is always called - * (but not always under beforeAll), - *

- * Method could be useful if some action must be performed before each test (in case of nested tests or - * global application when "start" would not be called for each test). - * - * @param context junit extension - * @see #getSupport(org.junit.jupiter.api.extension.ExtensionContext) - * @see #getInjector(org.junit.jupiter.api.extension.ExtensionContext) - * @see #getBean(org.junit.jupiter.api.extension.ExtensionContext, Class) - * @see #getClient(org.junit.jupiter.api.extension.ExtensionContext) - */ - default void beforeAll(final ExtensionContext context) { - // empty default - } - - /** - * Called before each test method execution. Guice injections into test instance already performed. - * Even if an application is created in beforeEach phase, this method would be called after application creation. - * - * @param context junit extension - * @see #getSupport(org.junit.jupiter.api.extension.ExtensionContext) - * @see #getInjector(org.junit.jupiter.api.extension.ExtensionContext) - * @see #getBean(org.junit.jupiter.api.extension.ExtensionContext, Class) - * @see #getClient(org.junit.jupiter.api.extension.ExtensionContext) - */ - default void beforeEach(final ExtensionContext context) { - // empty default - } - - /** - * Called after each test method execution. Even if an application is closed on afterEach, this method would be - * called before it. - * - * @param context junit extension - * @see #getSupport(org.junit.jupiter.api.extension.ExtensionContext) - * @see #getInjector(org.junit.jupiter.api.extension.ExtensionContext) - * @see #getBean(org.junit.jupiter.api.extension.ExtensionContext, Class) - * @see #getClient(org.junit.jupiter.api.extension.ExtensionContext) - */ - default void afterEach(final ExtensionContext context) { - // empty default - } - - /** - * IMPORTANT: this method MIGHT NOT BE CALLED at all in case if extension is registered under non-static field - * (and so the application is stopped after each method). - * Prefer {@link #stopped(org.junit.jupiter.api.extension.ExtensionContext)} instead, which is always called (but - * not always under afterAll), - *

- * Method could be useful if some action must be performed after each test (in case of nested tests or - * global application when "stop" would not be called for each test). - * - * @param context junit extension - * @see #getSupport(org.junit.jupiter.api.extension.ExtensionContext) - * @see #getInjector(org.junit.jupiter.api.extension.ExtensionContext) - * @see #getBean(org.junit.jupiter.api.extension.ExtensionContext, Class) - * @see #getClient(org.junit.jupiter.api.extension.ExtensionContext) - */ - default void afterAll(final ExtensionContext context) { - // empty default - } - - /** - * Called when dropwizard (or guicey) application stopped. It could be afterAll or afterEach phase - * (if important, look {@link org.junit.jupiter.api.extension.ExtensionContext#getTestMethod()} to make sure). - * Application could start/stop multiple times within one test class (if extension registered in non-static field). - *

- * Note that in case of global application usage or for nested tests this method might not be called because - * application lifecycle would be managed by the top-most test. - *

- * This method could be used instead of afterAll because normally extension is stopped under afterAll, but - * for extensions stopped under afterEach - it would be impossible to notify about afterAll anyway. - * - * @param context junit extension context - * @see #getSupport(org.junit.jupiter.api.extension.ExtensionContext) - * @see #getInjector(org.junit.jupiter.api.extension.ExtensionContext) - * @see #getBean(org.junit.jupiter.api.extension.ExtensionContext, Class) - * @see #getClient(org.junit.jupiter.api.extension.ExtensionContext) - */ - default void stopped(final ExtensionContext context) { - // empty default - } - - /** - * Shortcut method to avoid passing support as a parameter in each method. Normally, it is impossible that support - * would not be found (under called lifecycle methods) - * - * @param context junit extension context - * @return dropwizard support object (or guicey support) - * @throws IllegalStateException if the support object not found (should be impossible) - */ - default DropwizardTestSupport getSupport(final ExtensionContext context) { - return GuiceyExtensionsSupport.lookupSupport(context) - .orElseThrow(() -> new IllegalStateException("Test support not found")); - } - - /** - * Shortcut method to avoid passing injector as a parameter in each method. Normally, it is impossible that - * injector would not be found (under called lifecycle methods) - * - * @param context junit extension context - * @return injector instance - * @throws IllegalStateException if the injector object not found (should be impossible) - */ - default Injector getInjector(final ExtensionContext context) { - return GuiceyExtensionsSupport.lookupInjector(context) - .orElseThrow(() -> new IllegalStateException("Injector not found")); - } - - /** - * Shortcut to get bean directly from injector. - * - * @param context junit extension context - * @param type bean class - * @param bean type - * @return bean instance, never null (throw error if not found) - */ - default T getBean(final ExtensionContext context, final Class type) { - return getInjector(context).getInstance(type); - } - - /** - * Note that client is created even for pure guicey tests (in case if something external must be called). - * - * @param context junit extension context - * @return client instance - * @throws IllegalStateException if the client object not found (should be impossible) - */ - default ClientSupport getClient(final ExtensionContext context) { - return GuiceyExtensionsSupport.lookupClient(context) - .orElseThrow(() -> new IllegalStateException("Client not found")); - } -} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestExtension.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestExtension.java index a1af36bb5..a4c0e74a7 100644 --- a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestExtension.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestExtension.java @@ -1,6 +1,10 @@ package ru.vyarus.dropwizard.guice.test.jupiter.env; import org.junit.jupiter.api.extension.ExtensionContext; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda.ListenerEvent; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda.LambdaTestListener; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda.TestExecutionListenerLambdaAdapter; import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.ExtensionBuilder; import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.ExtensionConfig; import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; @@ -19,6 +23,7 @@ public class TestExtension extends ExtensionBuilder + * Listener could also be registered with lambdas using on* methods like + * {@link #onApplicationStart(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda.LambdaTestListener)}. + * Lambda version might be more convenient in case when setup object is a lambda itself. * * @param listener listener object * @return builder instance for chained calls @@ -79,6 +88,102 @@ public TestExtension listen(final TestExecutionListener listener) { return this; } + /** + * Lambda version of {@link #listen(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)} + * for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener#started( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)}. Lambda listener version is more useful in + * case when setup object is declared as a lambda itself. + *

+ * Might be called multiple times. + * + * @param listener listener called after application start (could be beforeAll (default) or beforeEach phase) + * @return builder instance for chained calls + */ + public TestExtension onApplicationStart(final LambdaTestListener listener) { + registerListener(ListenerEvent.Started, listener); + return this; + } + + /** + * Lambda version of {@link #listen(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)} + * for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener#beforeAll( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)}. Lambda listener version is more useful in + * case when setup object is declared as a lambda itself. + *

+ * Might be called multiple times. + * + * @param listener listener called (might not be called!) before all test methods + * @return builder instance for chained calls + */ + public TestExtension onBeforeAll(final LambdaTestListener listener) { + registerListener(ListenerEvent.BeforeAll, listener); + return this; + } + + /** + * Lambda version of {@link #listen(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)} + * for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener#beforeEach( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)}. Lambda listener version is more useful in + * case when setup object is declared as a lambda itself. + *

+ * Might be called multiple times. + * + * @param listener listener called before each test method + * @return builder instance for chained calls + */ + public TestExtension onBeforeEach(final LambdaTestListener listener) { + registerListener(ListenerEvent.BeforeEach, listener); + return this; + } + + /** + * Lambda version of {@link #listen(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)} + * for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener#afterEach( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)}. Lambda listener version is more useful in + * case when setup object is declared as a lambda itself. + *

+ * Might be called multiple times. + * + * @param listener listener called after each test method + * @return builder instance for chained calls + */ + public TestExtension onAfterEach(final LambdaTestListener listener) { + registerListener(ListenerEvent.AfterEach, listener); + return this; + } + + /** + * Lambda version of {@link #listen(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)} + * for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener#afterAll( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)}. Lambda listener version is more useful in + * case when setup object is declared as a lambda itself. + *

+ * Might be called multiple times. + * + * @param listener listener called (might not be called!) after all test methods + * @return builder instance for chained calls + */ + public TestExtension onAfterAll(final LambdaTestListener listener) { + registerListener(ListenerEvent.AfterAll, listener); + return this; + } + + /** + * Lambda version of {@link #listen(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)} + * for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener#stopped( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)}. Lambda listener version is more useful in + * case when setup object is declared as a lambda itself. + *

+ * Might be called multiple times. + * + * @param listener listener called after application stop (could be afterAll (default) or afterEach phase) + * @return builder instance for chained calls + */ + public TestExtension onApplicationStop(final LambdaTestListener listener) { + registerListener(ListenerEvent.Stopped, listener); + return this; + } + /** * Search for annotated fields with validation (a field type must be assignable to provided type). * @@ -93,4 +198,13 @@ public List> findAnnotatedFields( final Class requiredFieldType) { return TestFieldUtils.findAnnotatedFields(context.getRequiredTestClass(), annotation, requiredFieldType); } + + private void registerListener(final ListenerEvent event, final LambdaTestListener listener) { + // create adapter on demand and register once - all other lambdas will refer to the same adapter instance + if (listenerAdapter == null) { + listenerAdapter = new TestExecutionListenerLambdaAdapter(); + listen(listenerAdapter); + } + listenerAdapter.listen(event, listener); + } } diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/field/AnnotatedTestFieldSetup.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/field/AnnotatedTestFieldSetup.java index 09074e423..78f882e9d 100644 --- a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/field/AnnotatedTestFieldSetup.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/field/AnnotatedTestFieldSetup.java @@ -2,7 +2,6 @@ import com.google.inject.Binder; import com.google.inject.Binding; -import com.google.inject.Injector; import com.google.inject.Stage; import com.google.inject.spi.InstanceBinding; import com.google.inject.spi.ProviderInstanceBinding; @@ -12,8 +11,9 @@ import ru.vyarus.dropwizard.guice.GuiceBundle; import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; -import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExecutionListener; import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener; import ru.vyarus.dropwizard.guice.test.util.TestSetupUtils; import java.lang.annotation.Annotation; @@ -117,7 +117,7 @@ public void configure(final GuiceBundle.Builder builder) { * validated. Also, if guice context started per each test method, validation would be called only for the first * test method because fields would be searched just once - no need to validate each time. * - * @param context junit context (just in case) + * @param context junit context * @param field annotated fields */ protected abstract void validateDeclaration(ExtensionContext context, AnnotatedField field); @@ -154,15 +154,13 @@ public void configure(final GuiceBundle.Builder builder) { * when extension relies on AOP and so would not work. Such validation is impossible to do before (in time * of binding overrides). *

- * Called before {@link #getFieldValue(org.junit.jupiter.api.extension.ExtensionContext, AnnotatedField, - * com.google.inject.Injector)}. + * Called before {@link #getFieldValue(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext, + * AnnotatedField)}. * - * @param context junit context + * @param context event context * @param field annotated field - * @param injector guice injector */ - protected abstract void validateBinding(ExtensionContext context, - AnnotatedField field, Injector injector); + protected abstract void validateBinding(EventContext context, AnnotatedField field); /** * Get test field value (would be immediately injected into the test field). Called only if field was not @@ -170,17 +168,16 @@ protected abstract void validateBinding(ExtensionContext context, * from guice context (if guice was re-configured with module overrides). *

* Warning: not called for manually initialized fields (because value already set)! To validate binding use - * {@link #validateBinding(org.junit.jupiter.api.extension.ExtensionContext, AnnotatedField, - * com.google.inject.Injector)} method instead (which is called for all fields). + * {@link #validateBinding(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext, AnnotatedField)} + * method instead (which is called for all fields). *

* Application already completely started and test extension initialized at this moment (beforeEach test phase). * - * @param context junit context (just in case) + * @param context event context * @param field annotated field - * @param injector guice injector * @return created field value */ - protected abstract T getFieldValue(ExtensionContext context, AnnotatedField field, Injector injector); + protected abstract T getFieldValue(EventContext context, AnnotatedField field); /** * Called when debug is enabled on guicey extension to report registered fields. @@ -194,43 +191,43 @@ protected abstract void validateBinding(ExtensionContext context, * - {@link #FIELD_MANUAL} field value was initialized by user, otherwise automatic * - {@link #FIELD_INJECTED} field injection instance (test instance) * - * @param context junit context (just in case), IMPORTANT - this would be setup context and not current + * @param context event context, IMPORTANT - this would be setup context and not current * @param fields fields to report */ - protected abstract void report(ExtensionContext context, List> fields); + protected abstract void report(EventContext context, List> fields); /** * Called before each test to pre-process field value (if required). * - * @param context junit context (just in case) + * @param context event context * @param field filed descriptor * @param value value instance */ - protected abstract void beforeTest(ExtensionContext context, AnnotatedField field, T value); + protected abstract void beforeTest(EventContext context, AnnotatedField field, T value); /** * Called after each test to post-process field value (if required). * - * @param context junit context (just in case) + * @param context event context * @param field filed descriptor * @param value value instance */ - protected abstract void afterTest(ExtensionContext context, AnnotatedField field, T value); + protected abstract void afterTest(EventContext context, AnnotatedField field, T value); @Override - public void started(final ExtensionContext context) { + public void started(final EventContext context) { // here because manual stubs detection will appear only during injector startup if (debug && !fields.isEmpty()) { - report(setupContext, fields); + report(new EventContext(setupContext), fields); } } @Override - public void beforeAll(final ExtensionContext context) { + public void beforeAll(final EventContext context) { // inject static fields - final Class testClass = context.getRequiredTestClass(); + final Class testClass = context.getJunitContext().getRequiredTestClass(); if (testClass == regTestClass) { - injectValues(context, fields, null, getInjector(context)); + injectValues(context, fields, null); } else { // in case on nested tests - search for declared fields and fail because injector already created validateUnreachableFieldsInNestedTest(testClass); @@ -238,22 +235,22 @@ public void beforeAll(final ExtensionContext context) { } @Override - public void beforeEach(final ExtensionContext context) { + public void beforeEach(final EventContext context) { // inject non-static fields - final TestInstances testInstances = context.getRequiredTestInstances(); - injectValues(context, fields, testInstances, getInjector(context)); + final TestInstances testInstances = context.getJunitContext().getRequiredTestInstances(); + injectValues(context, fields, testInstances); // call lifecycle methods on stub if required valueLifecycle(context, fields, testInstances, true); } @Override - public void afterEach(final ExtensionContext context) { + public void afterEach(final EventContext context) { // call lifecycle methods on stub if required - valueLifecycle(context, fields, context.getRequiredTestInstances(), false); + valueLifecycle(context, fields, context.getJunitContext().getRequiredTestInstances(), false); } @Override - public void stopped(final ExtensionContext context) { + public void stopped(final EventContext context) { // after app shutdown clear static fields injected with guice-managed bean // otherwise it would be impossible to differentiate it from manual stub for the next test (on per method) fields.forEach(field -> { @@ -338,16 +335,14 @@ protected void collectOverrideBindings(final List> fields, * Inject field values into test instance (under beforeEach). User defined values stay as is. Value is injected * only once for test instance. * - * @param context junit context + * @param context event context * @param fields annotated fields * @param testInstances tests instances (might be several for nested tests) - * @param injector injector instance */ @SuppressWarnings({"CyclomaticComplexity", "PMD.SimplifiedTernary"}) - protected void injectValues(final ExtensionContext context, + protected void injectValues(final EventContext context, final List> fields, - final TestInstances testInstances, - final Injector injector) { + final TestInstances testInstances) { final boolean checkFieldValueInvisibleOnInitialization = testInstances != null && appPerClass; fields.forEach(field -> { if (checkFieldValueInvisibleOnInitialization && !field.isStatic()) { @@ -355,7 +350,7 @@ protected void injectValues(final ExtensionContext context, // and fail on wrong usage failIfInstanceFieldInitialized(field, testInstances); } - validateBinding(context, field, injector); + validateBinding(context, field); // exact instance required because it must be stored final Object instance = field.findRequiredInstance(testInstances); @@ -365,7 +360,7 @@ protected void injectValues(final ExtensionContext context, || (!field.isStatic() && instance == field.getCustomData(FIELD_INJECTED)); // static fields might be not initialized in beforeAll (so do it in beforeEach) if ((instance != null || field.isStatic()) && !isAlreadyInjected) { - field.setValue(instance, getFieldValue(context, field, injector)); + field.setValue(instance, getFieldValue(context, field)); field.setCustomData(FIELD_INJECTED, field.isStatic() ? true : instance); } }); @@ -379,7 +374,7 @@ protected void injectValues(final ExtensionContext context, * @param testInstances test instances (might be several for nested tests) * @param before true for beforeEach, false for afterEach */ - protected void valueLifecycle(final ExtensionContext context, + protected void valueLifecycle(final EventContext context, final List> fields, final TestInstances testInstances, final boolean before) { diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/EventContext.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/EventContext.java new file mode 100644 index 000000000..96f973ac6 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/EventContext.java @@ -0,0 +1,75 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.env.listen; + +import com.google.inject.Injector; +import io.dropwizard.testing.DropwizardTestSupport; +import org.junit.jupiter.api.extension.ExtensionContext; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.GuiceyExtensionsSupport; + +/** + * Event context wraps junit {@link org.junit.jupiter.api.extension.ExtensionContext} and provides access for + * the main test objects (like injector, test support and http client). Custom object is required mainly for + * lambda even listeners, which are unable to see default interface methods. + * + * @author Vyacheslav Rusakov + * @since 20.02.2025 + */ +public class EventContext { + + private final ExtensionContext context; + + public EventContext(final ExtensionContext context) { + this.context = context; + } + + /** + * @return junit context + */ + public ExtensionContext getJunitContext() { + return context; + } + + /** + * Normally, it is impossible that support would not be found (under called lifecycle methods). + * + * @return dropwizard support object (or guicey support) + * @throws IllegalStateException if the support object not found (should be impossible) + */ + public DropwizardTestSupport getSupport() { + return GuiceyExtensionsSupport.lookupSupport(context) + .orElseThrow(() -> new IllegalStateException("Test support not found")); + } + + /** + * Normally, it is impossible that injector would not be found (under called lifecycle methods). + * + * @return injector instance + * @throws IllegalStateException if the injector object not found (should be impossible) + */ + public Injector getInjector() { + return GuiceyExtensionsSupport.lookupInjector(context) + .orElseThrow(() -> new IllegalStateException("Injector not found")); + } + + /** + * Shortcut to get bean directly from injector. + * + * @param type bean class + * @param bean type + * @return bean instance, never null (throw error if not found) + */ + public T getBean(final Class type) { + return getInjector().getInstance(type); + } + + /** + * Note that client is created even for pure guicey tests (in case if something external must be called). + * + * @return client instance + * @throws IllegalStateException if the client object not found (should be impossible) + */ + public ClientSupport getClient() { + return GuiceyExtensionsSupport.lookupClient(context) + .orElseThrow(() -> new IllegalStateException("Client not found")); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/TestExecutionListener.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/TestExecutionListener.java new file mode 100644 index 000000000..a27096b35 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/TestExecutionListener.java @@ -0,0 +1,105 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.env.listen; + +/** + * Test listener allows listening for the main test events. There are no beforeAll and afterAll events directly + * because guicey extension could be created either on beforeAll or in beforeEach (depends on test configuration). + *

+ * Before/after test called before and after each test method. This could be used for custom setup/cleanup logic. + * BeforeAll and afterAll might not be called - use with caution (depends on extension registration). + *

+ * This interface provides a simple replacement for junit extension (synchronized with guicey lifecycle). + * + * @author Vyacheslav Rusakov + * @since 07.02.2025 + */ +public interface TestExecutionListener { + + /** + * Called when dropwizard (or guicey) application started. It could be beforeAll or beforeEach phase + * (if important, look {@link org.junit.jupiter.api.extension.ExtensionContext#getTestMethod()} to make sure). + * Application could start/stop multiple times within one test class (if extension registered in non-static field). + *

+ * NOTE: At this stage, injections not yet performed inside test instance. + *

+ * This method could be used instead of beforeAll because normally extension is created under beforeAll, but + * for extensions created under beforeEach - it would be impossible to notify about beforeAll anyway. + * + * @param context context object providing access to all required objects (junit context, injector, + * test support, etc.) + */ + default void started(final EventContext context) { + // empty default + } + + /** + * IMPORTANT: this method MIGHT NOT BE CALLED at all in case if extension is registered under non-static field + * (and so application created before each method). + * Prefer {@link #started(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)} instead, which is + * always called (but not always under beforeAll), + *

+ * Method could be useful if some action must be performed before each test (in case of nested tests or + * global application when "start" would not be called for each test). + * + * @param context context object providing access to all required objects (junit context, injector, + * test support, etc.) + */ + default void beforeAll(final EventContext context) { + // empty default + } + + /** + * Called before each test method execution. Guice injections into test instance already performed. + * Even if an application is created in beforeEach phase, this method would be called after application creation. + * + * @param context context object providing access to all required objects (junit context, injector, + * test support, etc.) + */ + default void beforeEach(final EventContext context) { + // empty default + } + + /** + * Called after each test method execution. Even if an application is closed on afterEach, this method would be + * called before it. + * + * @param context context object providing access to all required objects (junit context, injector, + * test support, etc.) + */ + default void afterEach(final EventContext context) { + // empty default + } + + /** + * IMPORTANT: this method MIGHT NOT BE CALLED at all in case if extension is registered under non-static field + * (and so the application is stopped after each method). + * Prefer {@link #stopped(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)} instead, which is + * always called (but not always under afterAll), + *

+ * Method could be useful if some action must be performed after each test (in case of nested tests or + * global application when "stop" would not be called for each test). + * + * @param context context object providing access to all required objects (junit context, injector, + * test support, etc.) + */ + default void afterAll(final EventContext context) { + // empty default + } + + /** + * Called when dropwizard (or guicey) application stopped. It could be afterAll or afterEach phase + * (if important, look {@link org.junit.jupiter.api.extension.ExtensionContext#getTestMethod()} to make sure). + * Application could start/stop multiple times within one test class (if extension registered in non-static field). + *

+ * Note that in case of global application usage or for nested tests this method might not be called because + * application lifecycle would be managed by the top-most test. + *

+ * This method could be used instead of afterAll because normally extension is stopped under afterAll, but + * for extensions stopped under afterEach - it would be impossible to notify about afterAll anyway. + * + * @param context context object providing access to all required objects (junit context, injector, + * test support, etc.) + */ + default void stopped(final EventContext context) { + // empty default + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/LambdaTestListener.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/LambdaTestListener.java new file mode 100644 index 000000000..2f21a8ca7 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/LambdaTestListener.java @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda; + +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; + +/** + * Lambda version for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener}. Requires + * {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda.TestExecutionListenerLambdaAdapter}. Assumed to + * be used with {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup} object, when its declared + * as lambda itself and complete listenere implementation ( + * {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension#listen( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)}) would look clumsy. + * + * @author Vyacheslav Rusakov + * @since 20.02.2025 + */ +@FunctionalInterface +public interface LambdaTestListener { + + /** + * Called on test event (see {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener}) + * for events description. + * + * @param context context object providing access to all required objects + */ + void onTestEvent(EventContext context); +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/ListenerEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/ListenerEvent.java new file mode 100644 index 000000000..ad1ce5e5f --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/ListenerEvent.java @@ -0,0 +1,12 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda; + +/** + * Test listener event definition for + * {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda.LambdaTestListener}. + * + * @author Vyacheslav Rusakov + * @since 20.02.2025 + */ +public enum ListenerEvent { + Started, Stopped, BeforeAll, AfterAll, BeforeEach, AfterEach +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/TestExecutionListenerLambdaAdapter.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/TestExecutionListenerLambdaAdapter.java new file mode 100644 index 000000000..0b02bb7d8 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/TestExecutionListenerLambdaAdapter.java @@ -0,0 +1,66 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener; + +/** + * An adapter for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener} to be able to + * register each listener method with a lambada (more suitable for builder style, rather than direct interface + * implementation). + * + * @author Vyacheslav Rusakov + * @since 20.02.2025 + */ +public class TestExecutionListenerLambdaAdapter implements TestExecutionListener { + + private final Multimap listeners = ArrayListMultimap.create(); + + /** + * Add lambda as an event listener. + * + * @param event target event + * @param listener listener to add + */ + public void listen(final ListenerEvent event, final LambdaTestListener listener) { + listeners.put(event, listener); + } + + @Override + public void started(final EventContext context) { + callListeners(context, ListenerEvent.Started); + } + + @Override + public void beforeAll(final EventContext context) { + callListeners(context, ListenerEvent.BeforeAll); + } + + @Override + public void beforeEach(final EventContext context) { + callListeners(context, ListenerEvent.BeforeEach); + } + + @Override + public void afterEach(final EventContext context) { + callListeners(context, ListenerEvent.AfterEach); + } + + @Override + public void afterAll(final EventContext context) { + callListeners(context, ListenerEvent.AfterAll); + } + + @Override + public void stopped(final EventContext context) { + callListeners(context, ListenerEvent.Stopped); + } + + private void callListeners(final EventContext context, final ListenerEvent event) { + for (LambdaTestListener listener : listeners.get(event)) { + listener.onTestEvent(context); + } + } + +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/mock/MocksSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/mock/MocksSupport.java index f4fc9957c..951e6b02d 100644 --- a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/mock/MocksSupport.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/mock/MocksSupport.java @@ -2,13 +2,13 @@ import com.google.common.base.Preconditions; import com.google.inject.Binder; -import com.google.inject.Injector; import org.junit.jupiter.api.extension.ExtensionContext; import org.mockito.Mockito; import org.mockito.internal.util.MockUtil; import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedTestFieldSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; import ru.vyarus.dropwizard.guice.test.util.PrintUtils; import ru.vyarus.dropwizard.guice.test.util.TestSetupUtils; @@ -66,20 +66,18 @@ protected void bindField(final Binder binder, final AnnotatedField field, final Injector injector) { + protected void validateBinding(final EventContext context, final AnnotatedField field) { // nothing: no existing binding validation because jit injections might be used } @Override - protected Object getFieldValue(final ExtensionContext context, - final AnnotatedField field, final Injector injector) { + protected Object getFieldValue(final EventContext context, final AnnotatedField field) { return Preconditions.checkNotNull(field.getCustomData(FIELD_MOCK), "Mock not created"); } @Override @SuppressWarnings("PMD.SystemPrintln") - protected void report(final ExtensionContext context, + protected void report(final EventContext context, final List> annotatedFields) { final StringBuilder report = new StringBuilder("\nApplied mocks (@") .append(MockBean.class.getSimpleName()).append(") on ").append(setupContextName).append(":\n\n"); @@ -92,20 +90,20 @@ protected void report(final ExtensionContext context, } @Override - protected void beforeTest(final ExtensionContext context, + protected void beforeTest(final EventContext context, final AnnotatedField field, final Object value) { // only after test (mock might be used in setup) } @Override @SuppressWarnings("PMD.SystemPrintln") - protected void afterTest(final ExtensionContext context, + protected void afterTest(final EventContext context, final AnnotatedField field, final Object value) { if (field.getAnnotation().printSummary()) { final String res = Mockito.mockingDetails(value).printInvocations(); - System.out.println(PrintUtils.getPerformanceReportSeparator(context) + System.out.println(PrintUtils.getPerformanceReportSeparator(context.getJunitContext()) + "@" + MockBean.class.getSimpleName() + " stats on [After each] for " - + TestSetupUtils.getContextTestName(context) + ":\n\n" + + TestSetupUtils.getContextTestName(context.getJunitContext()) + ":\n\n" + Arrays.stream(res.split("\n")).map(s -> "\t" + s).collect(Collectors.joining("\n"))); } if (field.getAnnotation().autoReset()) { diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/spy/SpiesSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/spy/SpiesSupport.java index 09d72a8bb..73e520d81 100644 --- a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/spy/SpiesSupport.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/spy/SpiesSupport.java @@ -3,7 +3,6 @@ import com.google.common.base.Preconditions; import com.google.inject.Binder; import com.google.inject.Binding; -import com.google.inject.Injector; import com.google.inject.Provider; import com.google.inject.matcher.Matchers; import org.aopalliance.intercept.MethodInterceptor; @@ -13,6 +12,7 @@ import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedTestFieldSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; import ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean; import ru.vyarus.dropwizard.guice.test.util.PrintUtils; import ru.vyarus.dropwizard.guice.test.util.TestSetupUtils; @@ -51,7 +51,6 @@ protected void validateDeclaration(final ExtensionContext context, final Annotat } @Override - @SuppressWarnings("unchecked") protected void bindFieldValue(final Binder binder, final AnnotatedField field, final Object value) { throw new IllegalStateException(getDeclarationErrorPrefix(field) + "manual spy declaration is not supported. " @@ -68,17 +67,15 @@ protected void bindField(final Binder binder, final AnnotatedField field, final Injector injector) { + protected void validateBinding(final EventContext context, final AnnotatedField field) { final SpiedBean spy = Preconditions.checkNotNull(field.getCustomData(FIELD_SPY)); - final Binding binding = injector.getBinding(spy.getType()); + final Binding binding = context.getInjector().getBinding(spy.getType()); Preconditions.checkState(!isInstanceBinding(binding), getDeclarationErrorPrefix(field) + "target bean '%s' bound by instance and so can't be spied", spy.getType().getSimpleName()); } @Override - protected Object getFieldValue(final ExtensionContext context, - final AnnotatedField field, final Injector injector) { + protected Object getFieldValue(final EventContext context, final AnnotatedField field) { // inject already initialized spy from aop interceptor final SpiedBean aop = field.getCustomData(FIELD_SPY); return aop.getSpy(); @@ -86,7 +83,7 @@ protected Object getFieldValue(final ExtensionContext context, @Override @SuppressWarnings("PMD.SystemPrintln") - protected void report(final ExtensionContext context, final List> annotatedFields) { + protected void report(final EventContext context, final List> annotatedFields) { final StringBuilder report = new StringBuilder("\nApplied spies (@") .append(SpyBean.class.getSimpleName()).append(") on ").append(setupContextName).append(":\n\n"); fields.forEach(field -> report.append( @@ -97,20 +94,20 @@ protected void report(final ExtensionContext context, final List field, final Object value) { // only after test (spy might be used in setup) } @Override @SuppressWarnings("PMD.SystemPrintln") - protected void afterTest(final ExtensionContext context, + protected void afterTest(final EventContext context, final AnnotatedField field, final Object value) { if (field.getAnnotation().printSummary()) { final String res = Mockito.mockingDetails(value).printInvocations(); - System.out.println(PrintUtils.getPerformanceReportSeparator(context) + System.out.println(PrintUtils.getPerformanceReportSeparator(context.getJunitContext()) + "@" + SpyBean.class.getSimpleName() + " stats on [After each] for " - + TestSetupUtils.getContextTestName(context) + ":\n\n" + + TestSetupUtils.getContextTestName(context.getJunitContext()) + ":\n\n" + Arrays.stream(res.split("\n")).map(s -> "\t" + s).collect(Collectors.joining("\n"))); } if (field.getAnnotation().autoReset()) { diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/stub/StubsSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/stub/StubsSupport.java index b9a4b77de..01f7d7902 100644 --- a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/stub/StubsSupport.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/stub/StubsSupport.java @@ -1,11 +1,11 @@ package ru.vyarus.dropwizard.guice.test.jupiter.ext.stub; import com.google.inject.Binder; -import com.google.inject.Injector; import com.google.inject.Singleton; import org.junit.jupiter.api.extension.ExtensionContext; import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedTestFieldSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; import java.util.List; @@ -63,22 +63,19 @@ protected void bindField(final Binder binder, final AnnotatedField field, final Injector injector) { + protected void validateBinding(final EventContext context, final AnnotatedField field) { // nothing } @Override - protected Object getFieldValue(final ExtensionContext context, - final AnnotatedField field, - final Injector injector) { + protected Object getFieldValue(final EventContext context, final AnnotatedField field) { // if not declared, stub value created by guice - return injector.getInstance(field.getAnnotation().value()); + return context.getBean(field.getAnnotation().value()); } @Override @SuppressWarnings("PMD.SystemPrintln") - protected void report(final ExtensionContext context, + protected void report(final EventContext context, final List> fields) { final StringBuilder report = new StringBuilder("\nApplied stubs (@") .append(StubBean.class.getSimpleName()).append(") on ").append(setupContextName).append(":\n\n"); @@ -92,7 +89,7 @@ protected void report(final ExtensionContext context, } @Override - protected void beforeTest(final ExtensionContext context, + protected void beforeTest(final EventContext context, final AnnotatedField field, final Object value) { if (value instanceof StubLifecycle) { @@ -101,7 +98,7 @@ protected void beforeTest(final ExtensionContext context, } @Override - protected void afterTest(final ExtensionContext context, + protected void afterTest(final EventContext context, final AnnotatedField field, final Object value) { if (value instanceof StubLifecycle) { diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/track/TrackersSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/track/TrackersSupport.java index 32991136b..91fd4ff42 100644 --- a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/track/TrackersSupport.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/track/TrackersSupport.java @@ -5,7 +5,6 @@ import com.google.common.base.Stopwatch; import com.google.inject.Binder; import com.google.inject.Binding; -import com.google.inject.Injector; import com.google.inject.matcher.Matchers; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -13,6 +12,7 @@ import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedTestFieldSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.stat.TrackerStats; import ru.vyarus.dropwizard.guice.test.util.PrintUtils; import ru.vyarus.dropwizard.guice.test.util.TestSetupUtils; @@ -72,23 +72,21 @@ protected void bindField(final Binder binder, final AnnotatedField field, final Injector injector) { + protected void validateBinding(final EventContext context, final AnnotatedField field) { final Tracker tracker = Preconditions.checkNotNull(field.getCustomData(FIELD_TRACKER)); - final Binding binding = injector.getBinding(tracker.getType()); + final Binding binding = context.getInjector().getBinding(tracker.getType()); Preconditions.checkState(!isInstanceBinding(binding), getDeclarationErrorPrefix(field) + "target bean '%s' bound by instance and so can't be tracked", tracker.getType().getSimpleName()); } @Override - protected Tracker getFieldValue(final ExtensionContext context, - final AnnotatedField field, final Injector injector) { + protected Tracker getFieldValue(final EventContext context, final AnnotatedField field) { return field.getCustomData(FIELD_TRACKER); } @Override @SuppressWarnings("PMD.SystemPrintln") - protected void report(final ExtensionContext context, + protected void report(final EventContext context, final List> annotatedFields) { final StringBuilder report = new StringBuilder("\nApplied trackers (@") .append(TrackBean.class.getSimpleName()).append(") on ").append(setupContextName).append(DOUBLE_NL); @@ -100,23 +98,23 @@ protected void report(final ExtensionContext context, } @Override - protected void beforeTest(final ExtensionContext context, + protected void beforeTest(final EventContext context, final AnnotatedField field, final Tracker value) { // no clean to keep tracks from setup stage } @Override @SuppressWarnings("PMD.SystemPrintln") - protected void afterTest(final ExtensionContext context, + protected void afterTest(final EventContext context, final AnnotatedField field, final Tracker value) { final TrackBean ann = field.getAnnotation(); if (ann.printSummary()) { final Tracker tracker = field.getCustomData(FIELD_TRACKER); // report for exact tracker (activated with the annotation option - works without debug enabling) if (!tracker.isEmpty()) { - System.out.println(PrintUtils.getPerformanceReportSeparator(context) + System.out.println(PrintUtils.getPerformanceReportSeparator(context.getJunitContext()) + "Tracker<" + tracker.getType().getSimpleName() + ">" + " stats (sorted by median) for " - + TestSetupUtils.getContextTestName(context) + DOUBLE_NL + + TestSetupUtils.getContextTestName(context.getJunitContext()) + DOUBLE_NL + tracker.getStats().render()); } } @@ -127,16 +125,16 @@ protected void afterTest(final ExtensionContext context, @Override @SuppressWarnings("PMD.SystemPrintln") - public void afterEach(final ExtensionContext context) { + public void afterEach(final EventContext context) { if (debug) { final Tracker[] trackers = fields.stream() .map(field -> (Tracker) field.getCustomData(FIELD_TRACKER)) .toArray(Tracker[]::new); if (trackers.length > 0) { // report all trackers - works only with debug - System.out.println(PrintUtils.getPerformanceReportSeparator(context) + System.out.println(PrintUtils.getPerformanceReportSeparator(context.getJunitContext()) + "Trackers stats (sorted by median) for " - + TestSetupUtils.getContextTestName(context) + DOUBLE_NL + + TestSetupUtils.getContextTestName(context.getJunitContext()) + DOUBLE_NL + new TrackerStats(trackers).render()); } } diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/TestSetupUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/TestSetupUtils.java index 396064d25..74a52a215 100644 --- a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/TestSetupUtils.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/TestSetupUtils.java @@ -6,7 +6,7 @@ import ru.vyarus.dropwizard.guice.module.installer.util.InstanceUtils; import ru.vyarus.dropwizard.guice.test.jupiter.env.ListenersSupport; import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; -import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExecutionListener; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener; import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.ExtensionConfig; import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track.GuiceyTestTime; diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ListenerLambdaTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ListenerLambdaTest.java new file mode 100644 index 000000000..1f0d26ae9 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ListenerLambdaTest.java @@ -0,0 +1,156 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup; + +import com.google.common.base.Preconditions; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 20.02.2025 + */ +public class ListenerLambdaTest { + public static List actions = new ArrayList<>(); + + + @Test + void checkListeners() { + actions.clear(); + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors(selectClass(Test1.class)) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + err.printStackTrace(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(Arrays.asList( + "started", "beforeAll", "beforeEach", "afterEach", "beforeEach", "afterEach", "stopped", "afterAll"), actions); + } + + + @Test + void checkListenersForPerMethod() { + actions.clear(); + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors(selectClass(Test2.class)) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + err.printStackTrace(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(Arrays.asList( + "started", "beforeEach", "stopped", "afterEach", "started", "beforeEach", "stopped", "afterEach"), actions); + } + + @TestGuiceyApp(value = DefaultTestApp.class, debug = true, setup = Setup.class) + @Disabled // prevent direct execution + public static class Test1 { + + @Test + void fooTest() { + } + + @Test + void fooTest2() { + } + } + + @Disabled // prevent direct execution + public static class Test2 { + + @RegisterExtension + TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(DefaultTestApp.class) + .setup(Setup.class) + .debug() + .create(); + + @Test + void fooTest() { + } + + @Test + void fooTest2() { + } + } + + public static class Setup implements TestEnvironmentSetup { + @Override + public Object setup(TestExtension extension) { + return extension + .onApplicationStart(context -> { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + Preconditions.checkNotNull(context.getBean(GuiceyConfigurationInfo.class).getActiveScopes()); + actions.add("started"); + }) + .onBeforeAll(context -> { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + actions.add("beforeAll"); + }) + .onBeforeEach(context -> { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + actions.add("beforeEach"); + }) + .onAfterEach(context -> { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + actions.add("afterEach"); + }) + .onAfterAll(context -> { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + actions.add("afterAll"); + }) + .onApplicationStop(context -> { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + actions.add("stopped"); + }); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ListenerTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ListenerTest.java index 561968734..58fa75508 100644 --- a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ListenerTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ListenerTest.java @@ -8,7 +8,6 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.testkit.engine.EngineTestKit; @@ -16,8 +15,9 @@ import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; -import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExecutionListener; import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener; import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; import java.util.ArrayList; @@ -123,57 +123,63 @@ public static class Setup implements TestEnvironmentSetup { public Object setup(TestExtension extension) { extension.listen(new TestExecutionListener() { @Override - public void started(ExtensionContext context) { + public void started(EventContext context) { Preconditions.checkNotNull(context); - getSupport(context); - getInjector(context); - getClient(context); - getBean(context, GuiceyConfigurationInfo.class).getActiveScopes(); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + Preconditions.checkNotNull(context.getBean(GuiceyConfigurationInfo.class).getActiveScopes()); actions.add("started"); } @Override - public void beforeAll(ExtensionContext context) { + public void beforeAll(EventContext context) { Preconditions.checkNotNull(context); - getSupport(context); - getInjector(context); - getClient(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); actions.add("beforeAll"); } @Override - public void beforeEach(ExtensionContext context) { + public void beforeEach(EventContext context) { Preconditions.checkNotNull(context); - getSupport(context); - getInjector(context); - getClient(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); actions.add("beforeEach"); } @Override - public void afterEach(ExtensionContext context) { + public void afterEach(EventContext context) { Preconditions.checkNotNull(context); - getSupport(context); - getInjector(context); - getClient(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); actions.add("afterEach"); } @Override - public void afterAll(ExtensionContext context) { + public void afterAll(EventContext context) { Preconditions.checkNotNull(context); - getSupport(context); - getInjector(context); - getClient(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); actions.add("afterAll"); } @Override - public void stopped(ExtensionContext context) { + public void stopped(EventContext context) { Preconditions.checkNotNull(context); - getSupport(context); - getInjector(context); - getClient(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); actions.add("stopped"); } }); diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupObjectAutoRegistrationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupObjectAutoRegistrationTest.java index f66754a12..657acf894 100644 --- a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupObjectAutoRegistrationTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupObjectAutoRegistrationTest.java @@ -6,13 +6,13 @@ import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; import ru.vyarus.dropwizard.guice.GuiceBundle; import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; -import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExecutionListener; import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener; import java.util.ArrayList; import java.util.Arrays; @@ -62,7 +62,7 @@ public void configure(GuiceBundle.Builder builder) { } @Override - public void beforeEach(ExtensionContext context) { + public void beforeEach(EventContext context) { actions.add("beforeEach"); } }