diff --git a/libraries.gradle b/libraries.gradle
index 1d3b9f2cdf..c7138860fd 100644
--- a/libraries.gradle
+++ b/libraries.gradle
@@ -27,10 +27,17 @@ ext {
orgjsonVersion = '20200518'
postgresqlEmbeddedVersion = '1.2.8'
postgresqlBinVersion = '12.3.0'
+ //testing
+ jupiterVersion = '5.5.2'
+ mockitoVersion = '3.12.4'
libraries = [
//jUnit (Tests)
- junit: 'org.junit.jupiter:junit-jupiter:5.5.2',
+ junit: "org.junit.jupiter:junit-jupiter:${jupiterVersion}",
+ jupiter_api: "org.junit.jupiter:junit-jupiter-api:${jupiterVersion}",
+
+ //mockito (Tests)
+ mockito: "org.mockito:mockito-core:${mockitoVersion}",
//jPOS
jpos: "org.jpos:jpos:${jposVersion}",
@@ -99,7 +106,6 @@ ext {
jetty_servlets: "org.eclipse.jetty:jetty-servlets:${jettyVersion}",
jetty_ajp: "org.eclipse.jetty:jetty-ajp:${jettyVersion}",
jetty_continuation: "org.eclipse.jetty:jetty-continuation:${jettyVersion}",
- jetty_rewrite: "org.eclipse.jetty:jetty-rewrite:${jettyVersion}",
// Quartz Scheduler
quartz: 'org.quartz-scheduler:quartz:2.3.2',
diff --git a/modules/testcore/README.md b/modules/testcore/README.md
new file mode 100644
index 0000000000..4a0ddac39b
--- /dev/null
+++ b/modules/testcore/README.md
@@ -0,0 +1,75 @@
+# Module for aiding in unit tests
+
+This module provides annotations to inject some mock or frequently needed objects during testing.
+
+For example, it provides an injection for a `Log` object, and a `MUX` mock.
+
+## Log injection example
+In the following example, if the logger does not already exist, a default one, that logs to standard output is created with the given `logger` name, and assinged to the `Log` instance.
+```java
+@ExtendWith(LogSupplierExtension.class)
+class LogTest {
+ @LogSource(logger = "Q2", realm = "log-test")
+ Log log;
+
+ @Test
+ public void testDebug() {
+ log.debug("debug called");
+ }
+}
+```
+
+
+## Mux mocking injection example
+
+This test class is actually executed in this module's test.
+
+```java
+package org.jpos.ee.test;
+
+import org.jpos.iso.ISOException;
+import org.jpos.iso.ISOMsg;
+import org.jpos.iso.MUX;
+import org.jpos.q2.iso.QMUX;
+import org.jpos.util.NameRegistrar;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MUXSupplierExtension.class)
+class MUXSupplierExtensionTest {
+
+ private static final String MUX_NAME = "connected-mux";
+ @MUXMock(connected = true, name = MUX_NAME)
+ MUX connectedMux;
+
+ @MUXMock(connected = false)
+ MUX disconnectedMux;
+
+ @Test
+ void testConnectedMux() throws NameRegistrar.NotFoundException {
+ assertSame(connectedMux, QMUX.getMUX(MUX_NAME));
+ assertTrue(connectedMux.isConnected());
+ }
+
+ @Test
+ void testDisconnectedMux() {
+ assertFalse(disconnectedMux.isConnected());
+ }
+
+ @Test
+ void testMockRequest() throws NameRegistrar.NotFoundException, ISOException {
+ ISOMsg request = new ISOMsg("2100");
+ ISOMsg response = new ISOMsg("2110");
+ when(connectedMux.request(same(request), anyLong())).thenReturn(response);
+ MUX mux = QMUX.getMUX(MUX_NAME);
+ assertSame(connectedMux, mux);
+ assertTrue(mux.isConnected());
+ assertSame(response, mux.request(request, 1000L));
+ }
+}
+```
diff --git a/modules/testcore/build.gradle b/modules/testcore/build.gradle
new file mode 100644
index 0000000000..c31b6631a6
--- /dev/null
+++ b/modules/testcore/build.gradle
@@ -0,0 +1,7 @@
+description = 'jPOS-EE :: Testing Module'
+
+dependencies {
+ implementation libraries.jupiter_api
+ implementation libraries.mockito
+ implementation libraries.jpos
+}
diff --git a/modules/testcore/src/main/java/org/jpos/ee/test/LogSource.java b/modules/testcore/src/main/java/org/jpos/ee/test/LogSource.java
new file mode 100644
index 0000000000..e396e40bd5
--- /dev/null
+++ b/modules/testcore/src/main/java/org/jpos/ee/test/LogSource.java
@@ -0,0 +1,37 @@
+package org.jpos.ee.test;
+
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Injects a {@link org.jpos.util.Log} object in the declared member or parameter. It needs to be used alongside.
+ * {@code @ExtendWith(LogSupplierExtension.class)}
+ *
+ * Usage example: + *
{@code + * @ExtendWith(LogSupplierExtension.class) + * class XxxxTest ...{ + * ... + * @LogSource + * Log log; + * .... + * + * @AfterEach + * tearDown() { + * log.debug(...); + * } + * } + * }+ * + */ +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(LogSupplierExtension.class) +public @interface LogSource { + String realm() default ""; + String logger() default ""; +} diff --git a/modules/testcore/src/main/java/org/jpos/ee/test/LogSupplierExtension.java b/modules/testcore/src/main/java/org/jpos/ee/test/LogSupplierExtension.java new file mode 100644 index 0000000000..afbd5a8cf4 --- /dev/null +++ b/modules/testcore/src/main/java/org/jpos/ee/test/LogSupplierExtension.java @@ -0,0 +1,113 @@ +package org.jpos.ee.test; + +import org.jpos.util.Logger; +import org.junit.jupiter.api.extension.*; + +import java.lang.reflect.*; +import java.util.Arrays; +import java.util.function.BiConsumer; + + +public class LogSupplierExtension implements BeforeEachCallback, ParameterResolver, BeforeAllCallback { + + protected static void runOnFields(ExtensionContext context, boolean staticFields, BiConsumer
The {@link Method} or {@link Constructor} + * in which the parameter is declared can be retrieved via + * {@link ParameterContext#getDeclaringExecutable()}. + * + * @param parameterContext the context for the parameter for which an argument should + * be resolved; never {@code null} + * @param ignored unused + * @return {@code true} if this resolver can resolve an argument for the parameter + * @see #resolveParameter + * @see ParameterContext + */ + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext ignored) throws ParameterResolutionException { + return parameterContext.isAnnotated(LogSource.class) && parameterContext.getTarget().map(Object::getClass).filter(c -> c.isAssignableFrom(LogSource.class)).isPresent(); + } + + /** + * Resolve an argument for the parameter in the supplied {@link ParameterContext} + * for the supplied {@link ExtensionContext}. + * + *
This method is only called by the framework if {@link #supportsParameter} + * previously returned {@code true} for the same {@link ParameterContext} + * and {@link ExtensionContext}. + * + *
The {@link Method} or {@link Constructor} + * in which the parameter is declared can be retrieved via + * {@link ParameterContext#getDeclaringExecutable()}. + * + * @param parameterContext the context for the parameter for which an argument should + * be resolved; never {@code null} + * @param extensionContext the extension context for the {@code Executable} + * about to be invoked; never {@code null} + * @return the resolved argument for the parameter; may only be {@code null} if the + * parameter type is not a primitive + * @see #supportsParameter + * @see ParameterContext + */ + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return getLog(parameterContext.getParameter().getAnnotation(LogSource.class), extensionContext.getRequiredTestClass()); + } + + + /** + * Callback that is invoked once before all tests in the current + * container. + * + * @param context the current extension context; never {@code null} + */ + @Override + public void beforeAll(ExtensionContext context) throws Exception { + setUp(context, true); + } +} diff --git a/modules/testcore/src/main/java/org/jpos/ee/test/MUXMock.java b/modules/testcore/src/main/java/org/jpos/ee/test/MUXMock.java new file mode 100644 index 0000000000..9bad46d3fc --- /dev/null +++ b/modules/testcore/src/main/java/org/jpos/ee/test/MUXMock.java @@ -0,0 +1,54 @@ +package org.jpos.ee.test; + +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *
Injects a muck mux in the declared member or parameter. It needs to be used alongside
+ * {@code @ExtendWith(MUXSupplierExtension.class)}.
+ * Usage example: + *
+ *{@code + * @ExtendWith(MUXSupplierExtension.class) + * class XxxxTest ...{ + * ... + * @MUXMock //register the mux mock in name registrar, if not already registered, and injects it + * MUX mux + * .... + * + * testXxx() { + * //define return for some condition, this example uses mockito + * when(mux.request(same(request), anyLong())).thenReturn(response); + * } + * } + * }+ */ +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(MUXSupplierExtension.class) +public @interface MUXMock { + String MUX_NAME = ""; + + /** + *
Name by which the mux is going tobe registered in Name Registrar
+ * + *Don't use the same name for instance and static, unless you don't care the static is unregistered from + * {@link org.jpos.util.NameRegistrar}
+ * + * Defaults to {@code ""}, in which case one with a random name will be generated + * + * @return the name under which the mux mock will be registered. + */ + String name() default MUX_NAME; + + /** + * Tells if the mocked mux should return true when its {@code isConnected()} method is called. + * @return the value the mocked mux {@code isConnected()} method should return. + */ + boolean connected() default true; +} diff --git a/modules/testcore/src/main/java/org/jpos/ee/test/MUXSupplierExtension.java b/modules/testcore/src/main/java/org/jpos/ee/test/MUXSupplierExtension.java new file mode 100644 index 0000000000..a857f69124 --- /dev/null +++ b/modules/testcore/src/main/java/org/jpos/ee/test/MUXSupplierExtension.java @@ -0,0 +1,152 @@ +package org.jpos.ee.test; + +import org.jpos.iso.MUX; +import org.jpos.util.NameRegistrar; +import org.junit.jupiter.api.extension.*; + +import java.lang.reflect.*; +import java.util.Arrays; +import java.util.Random; +import java.util.function.BiConsumer; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MUXSupplierExtension implements BeforeEachCallback, AfterEachCallback, ParameterResolver, BeforeAllCallback, AfterAllCallback { + + protected static void runOnFields(ExtensionContext context, boolean staticFields, BiConsumerThe {@link Method} or {@link Constructor} + * in which the parameter is declared can be retrieved via + * {@link ParameterContext#getDeclaringExecutable()}. + * + * @param parameterContext the context for the parameter for which an argument should + * be resolved; never {@code null} + * @param extensionContext the extension context for the {@code Executable} + * about to be invoked; never {@code null} + * @return {@code true} if this resolver can resolve an argument for the parameter + * @see #resolveParameter + * @see ParameterContext + */ + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return parameterContext.isAnnotated(MUXMock.class); + } + + /** + * Resolve an argument for the parameter in the supplied {@link ParameterContext} + * for the supplied {@link ExtensionContext}. + * + *
This method is only called by the framework if {@link #supportsParameter} + * previously returned {@code true} for the same {@link ParameterContext} + * and {@link ExtensionContext}. + * + *
The {@link Method} or {@link Constructor} + * in which the parameter is declared can be retrieved via + * {@link ParameterContext#getDeclaringExecutable()}. + * + * @param parameterContext the context for the parameter for which an argument should + * be resolved; never {@code null} + * @param extensionContext the extension context for the {@code Executable} + * about to be invoked; never {@code null} + * @return the resolved argument for the parameter; may only be {@code null} if the + * parameter type is not a primitive + * @see #supportsParameter + * @see ParameterContext + */ + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return getMUXMock(parameterContext.getParameter().getAnnotation(MUXMock.class)); + } + + /** + * Callback that is invoked once after all tests in the current + * container. + * + * @param context the current extension context; never {@code null} + */ + @Override + public void afterAll(ExtensionContext context) throws Exception { + tearDown(context, true); + } + + /** + * Callback that is invoked once before all tests in the current + * container. + * + * @param context the current extension context; never {@code null} + */ + @Override + public void beforeAll(ExtensionContext context) throws Exception { + setUp(context, true); + } +} diff --git a/modules/testcore/src/main/java/org/jpos/ee/test/TestUtil.java b/modules/testcore/src/main/java/org/jpos/ee/test/TestUtil.java new file mode 100644 index 0000000000..f121c19677 --- /dev/null +++ b/modules/testcore/src/main/java/org/jpos/ee/test/TestUtil.java @@ -0,0 +1,18 @@ +package org.jpos.ee.test; + +import org.jpos.util.LogListener; +import org.jpos.util.Logger; +import org.jpos.util.SimpleLogListener; + +public class TestUtil { + private final static Logger logger = new Logger(); + + static { + LogListener stdout = new SimpleLogListener(); + logger.addListener(stdout); + } + + public static Logger getLogger() { + return logger; + } +} diff --git a/modules/testcore/src/test/java/org/jpos/ee/test/LogSupplierExtensionTest.java b/modules/testcore/src/test/java/org/jpos/ee/test/LogSupplierExtensionTest.java new file mode 100644 index 0000000000..b0a8b29454 --- /dev/null +++ b/modules/testcore/src/test/java/org/jpos/ee/test/LogSupplierExtensionTest.java @@ -0,0 +1,51 @@ +package org.jpos.ee.test; + +import org.jpos.util.Log; +import org.jpos.util.Logger; +import org.jpos.util.SimpleLogListener; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(LogSupplierExtension.class) +class LogSupplierExtensionTest { + static final String LOGGER_NAME = "test-logger"; + @LogSource (logger=LOGGER_NAME, realm="log-test") + Log log; + + Logger logger; + + SimpleLogListener logListener = new SimpleLogListener(); + + @BeforeEach + void setUp() { + logger = Logger.getLogger(LOGGER_NAME); + logListener = spy(new SimpleLogListener()); + logger.addListener(logListener); + } + + @AfterEach + void tearDown() { + logger.removeListener(logListener); + } + + @Test + public void testDebug(){ + log.debug("debug called"); + verify(logListener, times(1)).log(argThat(event -> { + assertSame(log, event.getSource()); + assertEquals(Log.DEBUG, event.getTag()); + assertEquals(1, event.getPayLoad().size()); + assertEquals("debug called", event.getPayLoad().get(0)); + return true; + })); + } + +} \ No newline at end of file diff --git a/modules/testcore/src/test/java/org/jpos/ee/test/MUXSupplierExtensionTest.java b/modules/testcore/src/test/java/org/jpos/ee/test/MUXSupplierExtensionTest.java new file mode 100644 index 0000000000..41ca3ecc0b --- /dev/null +++ b/modules/testcore/src/test/java/org/jpos/ee/test/MUXSupplierExtensionTest.java @@ -0,0 +1,46 @@ +package org.jpos.ee.test; + +import org.jpos.iso.ISOException; +import org.jpos.iso.ISOMsg; +import org.jpos.iso.MUX; +import org.jpos.q2.iso.QMUX; +import org.jpos.util.NameRegistrar; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.when; + +@ExtendWith(MUXSupplierExtension.class) +class MUXSupplierExtensionTest { + + private static final String MUX_NAME = "connected-mux"; + @MUXMock(connected = true, name = MUX_NAME) + MUX connectedMux; + + @MUXMock(connected = false) + MUX disconnectedMux; + @Test + void testConnectedMux() throws NameRegistrar.NotFoundException { + assertSame(connectedMux, QMUX.getMUX(MUX_NAME)); + assertTrue(connectedMux.isConnected()); + } + + @Test + void testDisconnectedMux() { + assertFalse(disconnectedMux.isConnected()); + } + + @Test + void testMockRequest() throws NameRegistrar.NotFoundException, ISOException { + ISOMsg request = new ISOMsg("2100"); + ISOMsg response = new ISOMsg("2110"); + when(connectedMux.request(same(request), anyLong())).thenReturn(response); + MUX mux = QMUX.getMUX(MUX_NAME); + assertSame(connectedMux, mux); + assertTrue(mux.isConnected()); + assertSame(response, mux.request(request, 1000L)); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index cfa0ad84a1..2a70dbda42 100644 --- a/settings.gradle +++ b/settings.gradle @@ -46,7 +46,8 @@ include ':modules:core', ':modules:seqno', ':modules:db-flyway', ':modules:elasticsearch', - ':modules:bom' + ':modules:bom', + ':modules:testcore' rootProject.name = 'jposee'