junit-team/junit5 · error · JUnitException

Detected cycle in inner class hierarchy between

Error message

Detected cycle in inner class hierarchy between %s and %s

What it means

JUnitException thrown by ReflectionUtils.detectInnerClassCycle via CycleErrorHandling.THROW_EXCEPTION when an inner class's superclass also appears in its enclosing-class chain - i.e. the inner-class hierarchy contains a cycle. Used during nested-test and @Nested discovery to prevent infinite recursion when walking enclosing classes.

Solutions

  1. Refactor the offending class so its enclosing chain does not include its superclass
  2. Use ReflectionUtils.isNestedClassPresent(..., CycleErrorHandling.ABORT_VISIT) to skip rather than throw
  3. Regenerate the bytecode without the cycle if a tool produced it
  4. File a bug against the bytecode tool if cycles appear without user action

Example fix

// before
class Outer {
  class Inner extends Outer {} // cycle: Inner's superclass is its enclosing class
}

// after - option A: break the cycle
class Outer {
  static class Inner {}
}

// after - option B: tolerate the cycle at scan time
ReflectionUtils.isNestedClassPresent(clazz, predicate, CycleErrorHandling.ABORT_VISIT);
Defensive patterns

Strategy: fallback

Try / catch

try {
    ReflectionUtils.isNestedClassPresent(clazz, predicate, CycleErrorHandling.THROW_EXCEPTION);
} catch (JUnitException e) {
    // fall back to skipping the cycle rather than aborting discovery
    ReflectionUtils.isNestedClassPresent(clazz, predicate, CycleErrorHandling.ABORT_VISIT);
}

Prevention

When it happens

Trigger: A class A.B (inner of A) whose superclass is A itself, or any inner class whose enclosing chain loops back through its superclass. Encountered by findNestedClasses / isNestedClassPresent when called with CycleErrorHandling.THROW_EXCEPTION (the default for several discovery predicates).

Common situations: Bytecode-engineered classes (ASM, ByteBuddy, Mockito mocks) that emit inner-class metadata creating a cycle; obfuscated classes; rarely hand-written pathological hierarchies; generated test fixtures.

Related errors


AI-assisted analysis of junit-team/junit5@f070c699a0 (2026-08-11). Data as JSON: /api/errors/af6ac804731850da. Report an issue: GitHub.

Appendix: source

Thrown at junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java:2022

	 * exception}; otherwise, this method returns the supplied {@code Throwable}.
	 */
	static Throwable getUnderlyingCause(Throwable t) {
		if (t instanceof InvocationTargetException ite) {
			return getUnderlyingCause(ite.getTargetException());
		}
		return t;
	}

	/**
	 * @since 1.13.2
	 */
	@API(status = INTERNAL, since = "1.13.2")
	public enum CycleErrorHandling {

		THROW_EXCEPTION {
			@Override
			void handle(Class<?> clazz, Class<?> enclosing) {
				throw new JUnitException("Detected cycle in inner class hierarchy between %s and %s".formatted(
					clazz.getName(), enclosing.getName()));
			}
		},

		ABORT_VISIT {
			@Override
			void handle(Class<?> clazz, Class<?> enclosing) {
				// ignore
			}
		};

		abstract void handle(Class<?> clazz, Class<?> enclosing);
	}

}

View on GitHub (pinned to f070c699a0)