Skip to content

MockWebServer @StartStop field on outer class not started for @Nested JUnit 5 tests #9546

Description

@BJMg

Problem

When a test class uses @Nested inner classes to group tests, a @StartStop MockWebServer field declared on the outer class is never started. Any test inside a nested class that touches the server (e.g. mockWebServer.url(...)) fails with:

java.lang.IllegalStateException: call start() first
    at mockwebserver3.MockWebServer.getSocketAddress(MockWebServer.kt:150)
    at mockwebserver3.MockWebServer.getHostName(MockWebServer.kt:156)
    at mockwebserver3.MockWebServer.url(MockWebServer.kt:200)

Minimal reproduction

class NestedStartStopTest {
    @StartStop
    private val mockWebServer = MockWebServer()

    @Nested
    inner class Group {
        @Test
        fun `server is running`() {
            // fails: IllegalStateException: call start() first
            mockWebServer.url("/")
        }
    }
}

Running the same assertion in a top-level @Test on the outer class works as expected.

Environment: com.squareup.okhttp3:mockwebserver3-junit5:5.3.2, JUnit Jupiter 5.x.

Root cause

In StartStopExtension.beforeEach:

override fun beforeEach(context: ExtensionContext) {
  val testInstance = context.testInstance.get()
  val instanceFields =
    findAnnotatedFields(
      context.requiredTestClass,
      StartStop::class.java,
    ) { !Modifier.isStatic(it.modifiers) }
  for (field in instanceFields) {
    ...
    val server = field.get(testInstance) as? MockWebServer ?: continue
    ...
    server.start()
  }
}

For a nested test:

  • context.testInstance.get() returns the inner-class instance, not the outer one that holds the field.
  • context.requiredTestClass is the inner @Nested class, so findAnnotatedFields never sees fields declared on enclosing classes.

Result: the outer @StartStop field is invisible to the extension, server.start() is never invoked, and any nested test that uses the server hits IllegalStateException.

Suggested fix

Iterate context.requiredTestInstances.allInstances (which contains outer instances in order) and, for each, enumerate @StartStop-annotated fields on that instance's class:

override fun beforeEach(context: ExtensionContext) {
  val store = context.getStore(Namespace.create(StartStop::class.java))
  for (testInstance in context.requiredTestInstances.allInstances) {
    val fields = findAnnotatedFields(
      testInstance.javaClass,
      StartStop::class.java,
    ) { !Modifier.isStatic(it.modifiers) }
    for (field in fields) {
      field.setAccessible(true)
      val server = field.get(testInstance) as? MockWebServer ?: continue
      store.put(field, server)
      server.start()
    }
  }
}

This matches the pattern JUnit 5 recommends for extensions that need to work with @Nested.

Current workarounds

  • Manual @BeforeEach/@AfterEach on the outer class calling mockWebServer.start()/.close(). Outer-class @BeforeEach runs before each nested test.
  • @TestInstance(Lifecycle.PER_CLASS) + @BeforeAll/@AfterAll — server shared across all nested tests.
  • Duplicate the @StartStop field on every @Nested class.
  • Drop @Nested and flatten the test class.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions