Milesoft OpenPOJO Extension (openpojo-ext)

1. Overview & Purpose

The OpenPOJO Extension (openpojo-ext) is a high-productivity unit testing utility built specifically to automate structural and behavioral validation of Plain Old Java Objects (POJOs) like Data Transfer Objects (DTOs), API models, and entities across the Milesoft ecosystem.

In enterprise Spring Boot applications and microservices, POJOs are used heavily for data contracts, persistence, and REST boundaries. Manually writing unit tests for every getter, setter, equals, hashCode, and toString method in dozens of POJOs is repetitive, error-prone, and leads to code coverage gaps.

Standard OpenPOJO provides excellent utilities for reflective testing, but it has certain limitations in modern JVM environments. The Milesoft openpojo-ext library extends standard OpenPOJO with:

  • Custom Rules & Testers: Custom, robust implementations of equals, hashCode, and toString rules and testers that integrate cleanly under the native com.openpojo namespace.
  • Convenient Testing Utilities: Standardized manual verification helpers (TestingUtils) for complex domain classes that require fine-grained assertion validation.
  • Smart Filtering: Intelligent filters (ExcludeTestsFilter, ExcludeClassesFilter) to seamlessly exclude test runners, inner test definitions, and helper utilities from automated package-level scans.

2. Installation & Dependency Configuration

The openpojo-ext library is hosted inside our secure Google Cloud Artifact Registry.

To pull this package into your downstream application or microservice, apply the Google Cloud Artifact Registry plugin and configure your build.gradle file:

2.1 Gradle Setup (build.gradle)

Add the plugin and repository configuration, then declare the dependency in your dependencies block:

plugins {
    id "com.google.cloud.artifactregistry.gradle-plugin" version "2.2.5"
    id "java-library"
}

repositories {
    mavenLocal()
    mavenCentral()
    maven {
        url "artifactregistry://us-west2-maven.pkg.dev/milesoft-repo/repo-maven"
    }
}

dependencies {
    // Required base foundation libraries
    implementation "io.milesoft:commons:2.0.1"
    
    // Milesoft OpenPOJO Extension
    testImplementation "io.milesoft:openpojo-ext:2.0.1"
}

Note: Because this library is exclusively designed for test automation, always register it using the testImplementation configuration scope.


3. Core Components Reference

The openpojo-ext library packages its custom rules, testers, and filters in the same namespaces as the parent OpenPOJO framework. This ensures that you can seamlessly substitute or mix them with standard OpenPOJO assertions.

3.1 Custom Reflection Filters

Filters prevent the reflection scanner from attempting to validate non-POJO classes (such as JUnit tests or nested test classes) that happen to live in the scanned package.

ExcludeTestsFilter

  • Fully Qualified Name: com.openpojo.reflection.filters.ExcludeTestsFilter
  • Purpose: Automatically filters out and excludes any classes in the scanned package whose names begin or end with "Test" (e.g., TestDtos itself, or nested helper classes).
  • Behavior: Extracts the base name of the class (stripping package structure and inner-class $ symbols) and performs standard prefix/suffix checks.

ExcludeClassesFilter

  • Fully Qualified Name: com.openpojo.reflection.filters.ExcludeClassesFilter
  • Purpose: Excludes a specified list of explicit classes from evaluation.
  • Usage: Pass an array of Class objects or a Collection<Class> to the constructor:
    new ExcludeClassesFilter(ExcludeMe.class, AnotherClassToSkip.class)
    

3.2 Custom Rules

Rules are structural checks evaluated before behavioral testing. They inspect class definitions using reflection to ensure specific methods or modifiers are defined.

EqualsMustExistRule

  • Fully Qualified Name: com.openpojo.validation.rule.impl.EqualsMustExistRule
  • Purpose: Asserts that every scanned POJO implements an equals method.
  • Failure Condition: Fails validation if the class does not declare an equals method, ensuring developer-defined or compiler-generated (e.g. Lombok @EqualsAndHashCode) identity logic exists.

HashCodeMustExistRule

  • Fully Qualified Name: com.openpojo.validation.rule.impl.HashCodeMustExistRule
  • Purpose: Asserts that every scanned POJO implements a hashCode method.
  • Failure Condition: Fails validation if no hashCode method is found, preserving the Java contract that equal objects must have equal hash codes.

ToStringMustExistRule

  • Fully Qualified Name: com.openpojo.validation.rule.impl.ToStringMustExistRule
  • Purpose: Asserts that every scanned POJO implements a custom toString method.
  • Failure Condition: Fails validation if no toString method is declared, preventing debugging logs from falling back to useless Object.toString() representations.

3.3 Custom Testers

Testers instantiate POJOs reflectively and assert their behavior at runtime.

EqualsTester

  • Fully Qualified Name: com.openpojo.validation.test.impl.EqualsTester
  • Purpose: Runs basic behavioral checks on the equals method.
  • Behavior: Obtains a basic reflective instance of the target POJO using ValidationHelper.getBasicInstance and asserts reflexivity: classInstance.equals(classInstance) must return true.

HashCodeTester

  • Fully Qualified Name: com.openpojo.validation.test.impl.HashCodeTester
  • Purpose: Runs behavioral checks on the hashCode method.
  • Behavior: Asserts determinism: multiple calls to hashCode() on the same instance must return the same integer value.

ToStringTester

  • Fully Qualified Name: com.openpojo.validation.test.impl.ToStringTester
  • Purpose: Runs behavioral checks on the toString method.
  • Behavior: Asserts that toString() returns a non-blank string (!StringUtils.isBlank(classInstance.toString())).

4. Manual Assertion Utilities (TestingUtils)

For complex domain objects, entity graphs, or models where automated scanner testing is too coarse, openpojo-ext provides the TestingUtils helper. This class offers exact, multi-invariant testing of equals, hashCode, and toString behaviors using developer-supplied test instances.

  • Fully Qualified Name: io.milesoft.testing.utils.TestingUtils

Method Signatures

Method Signature Description
testEqualsAndHashCode(I item, I same, I diff) Validates the standard Java identity contracts across three supplied instances.
testToString(I item) Asserts that toString() returns a valid, descriptive, and non-default representation.

How It Works

testEqualsAndHashCode Requirements

To use this method, you must supply three distinct, fully populated instances of your class I:

  1. item: The primary test subject.
  2. same: A distinct instance that is logically equivalent to item (should trigger equals() == true and equal hash codes).
  3. diff: A distinct instance that is logically different from item (should trigger equals() == false and unequal hash codes).

The method evaluates:

  • Reflexivity: item.equals(item) must be true.
  • Symmetry / Equality: item.equals(same) must be true.
  • Inequality: item.equals(diff) must be false.
  • Null Safety: item.equals(null) must be false.
  • Type Safety: item.equals(otherType) must be false.
  • HashCode Determinism: item.hashCode() == item.hashCode() must be true.
  • HashCode Consistency: item.hashCode() == same.hashCode() must be true.
  • HashCode Distribution: item.hashCode() == diff.hashCode() should ideally be false (fails if equal).

testToString Requirements

Passes if:

  • toString() is not null, empty, or blank.
  • toString() does not match the default Object.toString() format (java.lang.Object@<hex>), ensuring that your class has a human-readable override.

5. Real-World Integration Patterns

5.1 Automated Package-Level DTO Scanning (Standard)

This is the recommended strategy for DTO packages. Add a single test class (e.g. TestDtos) inside each DTO package to instantly validate all current and future DTOs.

import com.openpojo.reflection.filters.ExcludeTestsFilter;
import com.openpojo.reflection.filters.FilterPackageInfo;
import com.openpojo.validation.Validator;
import com.openpojo.validation.ValidatorBuilder;
import com.openpojo.validation.rule.impl.EqualsMustExistRule;
import com.openpojo.validation.rule.impl.GetterMustExistRule;
import com.openpojo.validation.rule.impl.HashCodeMustExistRule;
import com.openpojo.validation.rule.impl.NoPrimitivesRule;
import com.openpojo.validation.rule.impl.NoPublicFieldsRule;
import com.openpojo.validation.rule.impl.SetterMustExistRule;
import com.openpojo.validation.rule.impl.ToStringMustExistRule;
import com.openpojo.validation.test.impl.EqualsTester;
import com.openpojo.validation.test.impl.GetterTester;
import com.openpojo.validation.test.impl.HashCodeTester;
import com.openpojo.validation.test.impl.SetterTester;
import com.openpojo.validation.test.impl.ToStringTester;
import org.junit.Test;

public class TestDtos {

    // Automatically resolve the package of the test runner
    private static final String POJO_PACKAGE = TestDtos.class.getPackageName();

    @Test
    public void testPojoStructureAndBehavior() {
        Validator validator = ValidatorBuilder.create()
            // 1. Structural Checks (Rules)
            .with(new NoPublicFieldsRule())
            .with(new NoPrimitivesRule())
            .with(new GetterMustExistRule())
            .with(new SetterMustExistRule())
            .with(new EqualsMustExistRule())
            .with(new HashCodeMustExistRule())
            .with(new ToStringMustExistRule())
            
            // 2. Behavioral Checks (Testers)
            .with(new GetterTester())
            .with(new SetterTester())
            .with(new EqualsTester())
            .with(new HashCodeTester())
            .with(new ToStringTester())
            .build();

        // 3. Scan & Validate package with custom ExcludeTestsFilter
        validator.validate(
            POJO_PACKAGE, 
            new FilterPackageInfo(), 
            new ExcludeTestsFilter()
        );
    }
}

5.2 Manual Domain Model Assertion with TestingUtils

For complex, immutable domain objects or entities where package-level scanner execution is too broad, use TestingUtils in standard JUnit tests.

import io.milesoft.testing.utils.TestingUtils;
import org.junit.Test;

public class TestQualifier {

    @Test
    public void testQualifierInvariants() {
        // Create three distinct instances to verify the Java identity contracts
        Qualifier item = new Qualifier("key-1", "id-1");
        Qualifier same = new Qualifier("key-1", "id-1");
        Qualifier diff = new Qualifier("key-2", "id-2");

        // 1. Execute deep equals and hashCode assertions
        TestingUtils.testEqualsAndHashCode(item, same, diff);

        // 2. Execute toString completeness check
        TestingUtils.testToString(item);
    }
}

6. Troubleshooting & FAQs

Q: Why is my TestDtos test failing on itself or another test class?

A: This occurs when the scanner reads the class containing the JUnit test runner as a target POJO. Ensure you pass new ExcludeTestsFilter() inside your validator.validate() call. This filter will exclude any class starting or ending with "Test".

Q: Why does EqualsMustExistRule fail for a class that has Lombok's @Data or @EqualsAndHashCode?

A: If Lombok is properly configured, the equals method is generated at compile-time and should be detected. If it fails, ensure that:

  1. Annotation processing is enabled in your IDE / build environment.
  2. The compiler runs before test execution (e.g. classes under build/classes/java/main actually contain the generated bytecode).

Q: Why does EqualsTester fail with an AssertionError for classes with complex constructors?

A: OpenPOJO's ValidationHelper.getBasicInstance() attempts to instantiate classes reflectively. If a class requires complex, non-null dependencies in its constructor, reflective instantiation may fail or result in unstable default states. For such classes, avoid automated scans and use TestingUtils to execute manual tests using developer-constructed instances instead.