diff --git a/core/src/test/java/io/github/lengors/js2pets/rules/ConstructorRuleTest.java b/core/src/test/java/io/github/lengors/js2pets/rules/ConstructorRuleTest.java new file mode 100644 index 0000000..fa39bc8 --- /dev/null +++ b/core/src/test/java/io/github/lengors/js2pets/rules/ConstructorRuleTest.java @@ -0,0 +1,170 @@ +package io.github.lengors.js2pets.rules; + +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.jsonschema2pojo.Schema; +import org.jsonschema2pojo.rules.Rule; +import org.jsonschema2pojo.rules.RuleFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.fasterxml.jackson.databind.JsonNode; +import com.sun.codemodel.JClassAlreadyExistsException; +import com.sun.codemodel.JDefinedClass; + +import io.github.lengors.js2pets.annotators.EnhancedAnnotator; +import lombok.Getter; + +/** + * Unit tests for the {@link ConstructorRule} class. + * + * This test class verifies the correct behavior of the {@link ConstructorRule}, particularly its ability to include or + * exclude no-args constructors based on configuration and to notify annotators if required. + * + * @author lengors + */ +@ExtendWith(MockitoExtension.class) +class ConstructorRuleTest implements ConstructorRuleTestSuite { + + /** + * Mock rule used to simulate the super constructor rule behavior. + */ + @Mock + @Getter + @MonotonicNonNull + private Rule superConstructorRule; + + /** + * Mock JSON node used for testing. + */ + @Mock + @Getter + @MonotonicNonNull + private JsonNode node; + + /** + * Mock schema used for testing. + */ + @Mock + @Getter + @MonotonicNonNull + private Schema currentSchema; + + /** + * Mock rule factory used to provide configuration and utilities for testing. + */ + @Mock + @Getter + @MonotonicNonNull + private RuleFactory ruleFactory; + + /** + * Tests that the rule correctly includes a no-args constructor when configured to do so. + * + * @throws JClassAlreadyExistsException if a class with the same name already exists. + */ + @Test + void shouldCorrectlyIncludeNoArgsConstructor() throws JClassAlreadyExistsException { + testForSuccessWithoutPluginImplementation(true); + } + + /** + * Tests that the rule correctly excludes a no-args constructor when configured to do so. + * + * @throws JClassAlreadyExistsException if a class with the same name already exists. + */ + @Test + void shouldCorrectlyExcludeNoArgsConstructor() throws JClassAlreadyExistsException { + testForSuccessWithoutPluginImplementation(false); + } + + /** + * Tests that the rule fails to infer the inclusion of a no-args constructor when the Mojo configuration is not used. + * + * @throws JClassAlreadyExistsException if a class with the same name already exists. + */ + @Test + void shouldFailToInferNoArgsConstructorInclusionDueToMojoNotUsed() throws JClassAlreadyExistsException { + ConstructorRuleTestRunner + .prepareWithoutPluginImplementation(this, null) + .testForFailure(); + } + + /** + * Tests that the rule correctly infers the inclusion of a no-args constructor based on plugin configuration. + * + * @throws JClassAlreadyExistsException if a class with the same name already exists. + */ + @Test + void shouldCorrectlyInferIncludeNoArgsConstructor() throws JClassAlreadyExistsException { + testForSuccessWithPluginImplementation(true); + } + + /** + * Tests that the rule correctly infers the exclusion of a no-args constructor based on plugin configuration. + * + * @throws JClassAlreadyExistsException if a class with the same name already exists. + */ + @Test + void shouldCorrectlyInferExcludeNoArgsConstructor() throws JClassAlreadyExistsException { + testForSuccessWithPluginImplementation(false); + } + + /** + * Tests that the rule correctly infers the default inclusion of a no-args constructor. + * + * @throws JClassAlreadyExistsException if a class with the same name already exists. + */ + @Test + void shouldCorrectlyInferDefaultInclusionOfNoArgsConstructor() throws JClassAlreadyExistsException { + ConstructorRuleTestRunner + .prepare(this, 0) + .testForSuccess(true); + } + + /** + * Tests that the rule fails to infer the inclusion of a no-args constructor due to multiple executions. + * + * @throws JClassAlreadyExistsException if a class with the same name already exists. + */ + @Test + void shouldFailToInferNoArgsConstructorInclusionDueToMultipleExecutions() throws JClassAlreadyExistsException { + ConstructorRuleTestRunner + .prepare(this, 2) + .testForFailure(); + } + + /** + * Tests that the rule correctly notifies the annotator about the constructor if configured. + * + * @throws JClassAlreadyExistsException if a class with the same name already exists. + */ + @Test + void shouldCorrectlyNotifyAnnotator() throws JClassAlreadyExistsException { + final var annotator = Mockito.mock(EnhancedAnnotator.class); + + final var includeNoArgsConstructor = false; + final var runner = ConstructorRuleTestRunner.prepareWithoutPluginImplementation(this, includeNoArgsConstructor); + runner.testForSuccess(annotator, includeNoArgsConstructor); + + Mockito + .verify(annotator, Mockito.only()) + .constructor(runner.getArgsConstructor()); + } + + private void testForSuccessWithoutPluginImplementation(final boolean includeNoArgsConstructor) + throws JClassAlreadyExistsException { + ConstructorRuleTestRunner + .prepareWithoutPluginImplementation(this, includeNoArgsConstructor) + .testForSuccess(includeNoArgsConstructor); + } + + private void testForSuccessWithPluginImplementation(final boolean includeNoArgsConstructor) + throws JClassAlreadyExistsException { + ConstructorRuleTestRunner + .prepare(this, includeNoArgsConstructor) + .testForSuccess(includeNoArgsConstructor); + } +} diff --git a/core/src/test/java/io/github/lengors/js2pets/rules/ConstructorRuleTestRunner.java b/core/src/test/java/io/github/lengors/js2pets/rules/ConstructorRuleTestRunner.java new file mode 100644 index 0000000..cbb9885 --- /dev/null +++ b/core/src/test/java/io/github/lengors/js2pets/rules/ConstructorRuleTestRunner.java @@ -0,0 +1,239 @@ +package io.github.lengors.js2pets.rules; + +import org.apache.commons.collections4.IteratorUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.maven.model.Plugin; +import org.apache.maven.model.PluginExecution; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.descriptor.PluginDescriptor; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.codehaus.plexus.util.xml.Xpp3Dom; +import org.jsonschema2pojo.Annotator; +import org.jsonschema2pojo.GenerationConfig; +import org.jsonschema2pojo.InclusionLevel; +import org.jsonschema2pojo.Jackson2Annotator; +import org.jsonschema2pojo.rules.Rule; +import org.junit.jupiter.api.Assertions; +import org.mockito.Mockito; + +import com.sun.codemodel.JClassAlreadyExistsException; +import com.sun.codemodel.JCodeModel; +import com.sun.codemodel.JDefinedClass; +import com.sun.codemodel.JMethod; +import com.sun.codemodel.JMod; + +import io.github.lengors.js2pets.rules.exceptions.ConfigurationPropertyMissingException; +import lombok.Getter; + +import java.util.Map; +import java.util.Collections; + +/** + * A test runner class for testing the functionality of the {@link ConstructorRule}. * This class provides utilities to + * set up and run tests on the {@link ConstructorRule} by simulating different configurations and verifying the expected + * outcomes. + * + * @author lengors + */ +public final class ConstructorRuleTestRunner { + /** + * The node name used for applying the rule, initialized as an empty string. + */ + public static final String NODE_NAME = StringUtils.EMPTY; + + /** + * The rule under test for generating constructors in a defined class. + */ + private final Rule constructorRule; + + /** + * The configuration settings used for generating code. + */ + private final GenerationConfig generationConfig; + + /** + * The class being defined and modified in the tests. + */ + private final JDefinedClass definedClass; + + /** + * The test suite providing the mock components for testing. + */ + private final ConstructorRuleTestSuite js2petsConstructorRuleTestSuite; + + /** + * The no-arguments constructor generated in the test. + */ + private final JMethod noArgsConstructor; + + /** + * The arguments constructor generated in the test. + */ + @Getter + private final JMethod argsConstructor; + + private ConstructorRuleTestRunner( + final ConstructorRuleTestSuite js2petsConstructorRuleTestSuite, + final @Nullable Boolean includeNoArgsConstructor, + final boolean usePluginImplementation) throws JClassAlreadyExistsException { + final var codeModel = new JCodeModel(); + + this.js2petsConstructorRuleTestSuite = js2petsConstructorRuleTestSuite; + definedClass = codeModel._class("io.github.lengors.js2pets.rules.Test"); + noArgsConstructor = definedClass.constructor(JMod.PUBLIC); + argsConstructor = definedClass.constructor(JMod.PUBLIC); + argsConstructor.param(Integer.class, "test"); + + final var node = js2petsConstructorRuleTestSuite.getNode(); + final var currentSchema = js2petsConstructorRuleTestSuite.getCurrentSchema(); + final var superConstructorRule = js2petsConstructorRuleTestSuite.getSuperConstructorRule(); + Mockito + .when(superConstructorRule.apply(NODE_NAME, node, node, definedClass, currentSchema)) + .thenReturn(definedClass); + + generationConfig = usePluginImplementation + ? (GenerationConfig) Mockito.mock(AbstractMojo.class, Mockito + .withSettings() + .extraInterfaces(GenerationConfig.class)) + : Mockito.mock(GenerationConfig.class); + + if (includeNoArgsConstructor == null) { + Mockito + .when(js2petsConstructorRuleTestSuite + .getRuleFactory() + .getGenerationConfig()) + .thenReturn(generationConfig); + } + + constructorRule = new ConstructorRule( + js2petsConstructorRuleTestSuite.getRuleFactory(), + includeNoArgsConstructor, + superConstructorRule); + } + + private JDefinedClass apply() { + return constructorRule.apply( + NODE_NAME, + js2petsConstructorRuleTestSuite.getNode(), + js2petsConstructorRuleTestSuite.getNode(), + definedClass, + js2petsConstructorRuleTestSuite.getCurrentSchema()); + } + + /** + * Runs a success test on the rule with a custom annotator, verifying the inclusion of constructors. + * + * @param annotator The annotator used to annotate the class. + * @param expectedIncludeNoArgsConstructor Whether a no-args constructor should be included. + */ + public void testForSuccess(final Annotator annotator, final boolean expectedIncludeNoArgsConstructor) { + Mockito + .when(js2petsConstructorRuleTestSuite + .getRuleFactory() + .getAnnotator()) + .thenReturn(annotator); + + final var result = apply(); + final var constructors = IteratorUtils.toList(result.constructors()); + Assertions.assertEquals(expectedIncludeNoArgsConstructor, constructors.contains(noArgsConstructor)); + Assertions.assertTrue(constructors.contains(argsConstructor)); + } + + /** + * Runs a success test on the rule with a default Jackson2Annotator, verifying the inclusion of constructors. + * + * @param expectedIncludeNoArgsConstructor Whether a no-args constructor should be included. + */ + public void testForSuccess(final boolean expectedIncludeNoArgsConstructor) { + Mockito + .when(generationConfig.getInclusionLevel()) + .thenReturn(InclusionLevel.NON_NULL); + testForSuccess(new Jackson2Annotator(generationConfig), expectedIncludeNoArgsConstructor); + } + + /** + * Runs a failure test on the rule, expecting a {@link ConfigurationPropertyMissingException}. + */ + public void testForFailure() { + Assertions.assertThrows(ConfigurationPropertyMissingException.class, this::apply); + } + + /** + * Prepares a test runner for testing without using the plugin implementation. + * + * @param js2petsConstructorRuleTestSuite The test suite to use. + * @param includeNoArgsConstructor Whether to include a no-args constructor. + * @return A configured test runner instance. + * @throws JClassAlreadyExistsException If a class with the same name already exists. + */ + public static ConstructorRuleTestRunner prepareWithoutPluginImplementation( + final ConstructorRuleTestSuite js2petsConstructorRuleTestSuite, + final @Nullable Boolean includeNoArgsConstructor) throws JClassAlreadyExistsException { + return new ConstructorRuleTestRunner(js2petsConstructorRuleTestSuite, includeNoArgsConstructor, false); + } + + /** + * Prepares a test runner with a specified number of plugin executions. + * + * @param js2petsConstructorRuleTestSuite The test suite to use. + * @param pluginExecutionCount The number of plugin executions to simulate. + * @return A configured test runner instance. + * @throws JClassAlreadyExistsException If a class with the same name already exists. + */ + public static ConstructorRuleTestRunner prepare( + final ConstructorRuleTestSuite js2petsConstructorRuleTestSuite, + final int pluginExecutionCount) throws JClassAlreadyExistsException { + return prepare(js2petsConstructorRuleTestSuite, pluginExecutionCount, Mockito.mock(Plugin.class)); + } + + /** + * Prepares a test runner for testing with a plugin implementation. + * + * @param js2petsConstructorRuleTestSuite The test suite to use. + * @param includeNoArgsConstructor Whether to include a no-args constructor. + * @return A configured test runner instance. + * @throws JClassAlreadyExistsException If a class with the same name already exists. + */ + public static ConstructorRuleTestRunner prepare( + final ConstructorRuleTestSuite js2petsConstructorRuleTestSuite, + final boolean includeNoArgsConstructor) throws JClassAlreadyExistsException { + final var plugin = Mockito.mock(Plugin.class); + final var runner = prepare(js2petsConstructorRuleTestSuite, 1, plugin); + + final var rootDom = Mockito.mock(Xpp3Dom.class); + final var includeNoArgsConstructorDom = Mockito.mock(Xpp3Dom.class); + + Mockito + .when(plugin.getConfiguration()) + .thenReturn(rootDom); + Mockito + .when(rootDom.getChild("includeNoArgsConstructor")) + .thenReturn(includeNoArgsConstructorDom); + Mockito + .when(includeNoArgsConstructorDom.getValue()) + .thenReturn(String.valueOf(includeNoArgsConstructor)); + return runner; + } + + private static ConstructorRuleTestRunner prepare( + final ConstructorRuleTestSuite js2petsConstructorRuleTestSuite, + final int pluginExecutionCount, + final Plugin plugin) throws JClassAlreadyExistsException { + final var runner = new ConstructorRuleTestRunner(js2petsConstructorRuleTestSuite, null, true); + final var pluginDescriptor = Mockito.mock(PluginDescriptor.class); + final var pluginExecution = Mockito.mock(PluginExecution.class); + final var generatingConfig = (AbstractMojo) js2petsConstructorRuleTestSuite + .getRuleFactory() + .getGenerationConfig(); + Mockito + .when(generatingConfig.getPluginContext()) + .thenReturn(Map.of("pluginDescriptor", pluginDescriptor)); + Mockito + .when(pluginDescriptor.getPlugin()) + .thenReturn(plugin); + Mockito + .when(plugin.getExecutions()) + .thenReturn(Collections.nCopies(pluginExecutionCount, pluginExecution)); + return runner; + } +} diff --git a/core/src/test/java/io/github/lengors/js2pets/rules/ConstructorRuleTestSuite.java b/core/src/test/java/io/github/lengors/js2pets/rules/ConstructorRuleTestSuite.java new file mode 100644 index 0000000..4f1b712 --- /dev/null +++ b/core/src/test/java/io/github/lengors/js2pets/rules/ConstructorRuleTestSuite.java @@ -0,0 +1,45 @@ +package io.github.lengors.js2pets.rules; + +import org.jsonschema2pojo.Schema; +import org.jsonschema2pojo.rules.Rule; +import org.jsonschema2pojo.rules.RuleFactory; + +import com.fasterxml.jackson.databind.JsonNode; +import com.sun.codemodel.JDefinedClass; + +/** + * A test suite interface for providing necessary components to test the {@link ConstructorRule}. Implementations of + * this interface should supply mock or real instances of the required schema, rule factory, JSON nodes, and super + * constructor rules. These components are used by the {@link ConstructorRuleTestRunner} to perform unit tests. + * + * @author lengors + */ +public interface ConstructorRuleTestSuite { + /** + * Retrieves the current schema being tested. + * + * @return The schema instance used in the current test context. + */ + Schema getCurrentSchema(); + + /** + * Retrieves the rule factory used to create rules for schema to Java conversion. + * + * @return The rule factory instance used in the test. + */ + RuleFactory getRuleFactory(); + + /** + * Retrieves the JSON node representing the part of the schema being tested. + * + * @return The JSON node being tested. + */ + JsonNode getNode(); + + /** + * Retrieves the rule for generating the super constructor in a defined class. + * + * @return The rule used to generate a super constructor in the test. + */ + Rule getSuperConstructorRule(); +} diff --git a/core/src/test/java/io/github/lengors/js2pets/rules/package-info.java b/core/src/test/java/io/github/lengors/js2pets/rules/package-info.java new file mode 100644 index 0000000..3cf9fec --- /dev/null +++ b/core/src/test/java/io/github/lengors/js2pets/rules/package-info.java @@ -0,0 +1,9 @@ +/** + * This package contains the rules that extend the functionality of the JS2Pets library. + * + * These rules are used to customize the behavior of jsonschema2pojo, specifically in handling constructor generation + * and related tasks within the context of JSON Schema to POJO conversion. + * + * @author lengors + */ +package io.github.lengors.js2pets.rules;