junitjunit5spring-testspring-test-mvc

java.lang.NoClassDefFoundError: org/junit/rules/TestRule in JUnit 5


I have the following code used in Junit 5 test:

@WebAppConfiguration
public class BaseControllerTest {

  static {
    System.setProperty("org.jboss.logging.provider", "slf4j");
  }

  public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets");


}
.......


public class StandaloneControllerGetByIdTest
    extends BaseStandaloneControllerTest {

  @Test
  public void lookupStandaloneReturnsIsSuccessful() throws Exception {

    mockMvc.perform(
                    get("/accounts", "123", "04d71b9379ef4db49c28e113485ea76d")
                            .contentType(APPLICATION_JSON_VALUE))
            .andDo(print()).andExpect(status().isOk());
  }
}

I get error:

org/junit/rules/TestRule
java.lang.NoClassDefFoundError: org/junit/rules/TestRule
    at java.base/java.lang.ClassLoader.defineClass1(Native Method)
    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1027)
    at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150)
    at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:862)
    at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:760)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:681)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:639)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
    at java.base/java.lang.Class.getDeclaredFields0(Native Method)
    at java.base/java.lang.Class.privateGetDeclaredFields(Class.java:3473)
    at java.base/java.lang.Class.getDeclaredFields(Class.java:2542)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.lang.ClassNotFoundException: org.junit.rules.TestRule
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
    ... 13 more

Full code:

https://github.com/rcbandit111/private_class_poc/blob/main/src/test/java/com/test/core/web/rest/controller/standalone/StandaloneControllerGetByIdTest.java#L14

org/junit/rules/TestRule is for Junit 4. I need to use JUnit 5 dependencies. Do you know how I can fix this issue in Junit 5?


Solution

  • Root Cause:

    You're getting java.lang.NoClassDefFoundError: org/junit/rules/TestRule because you're using JUnitRestDocumentation, which depends on JUnit 4's TestRule, but you only have JUnit 5 (junit-jupiter) in your project.

    How to change it with JUnit5:

    import org.junit.jupiter.api.extension.ExtendWith;
    import org.springframework.restdocs.RestDocumentationContextProvider;
    import org.springframework.restdocs.RestDocumentationExtension;
    
    @ExtendWith(RestDocumentationExtension.class)
    public class MyTest {
    
        @BeforeEach
        void setUp(RestDocumentationContextProvider restDocumentation) {
            mockMvc = MockMvcBuilders.standaloneSetup(myController)
                    .apply(documentationConfiguration(restDocumentation))
                    .build();
        }
    }
    

    Here is the full example of the RestDocumentationExtension usage: (https://github.com/spring-projects/spring-restdocs/blob/v2.0.2.RELEASE/samples/junit5/src/test/java/com/example/junit5/SampleJUnit5ApplicationTests.java);


    In your case you have to do:

    1. Update your build.gradle file:
    plugins {
        id 'org.springframework.boot' version '3.4.0'
        id 'io.spring.dependency-management' version '1.1.7'
        id 'java'
        id 'org.asciidoctor.jvm.convert' version '2.4.0' // add this plugin
    
    }
    
    group = 'com.private.class.poc'
    version = '0.0.1'
    
    tasks.bootJar {
        archiveFileName.set('private-class-poc.jar')
    }
    
    java {
        sourceCompatibility = '21'
    }
    
    ext {
        set('springCloudVersion', "2024.0.0")
        set('snippetsDir', file("build/generated-snippets")) // package name for snippets
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
        implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
        implementation 'org.springframework.retry:spring-retry'
        implementation 'org.springframework.boot:spring-boot-starter-validation'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        implementation 'org.springframework.boot:spring-boot-starter-hateoas'
        implementation 'com.google.guava:guava:33.2.1-jre'
        implementation 'commons-validator:commons-validator:1.9.0'
        implementation 'org.apache.commons:commons-lang3:3.14.0'
        implementation 'commons-io:commons-io:2.18.0'
        implementation 'org.springframework.boot:spring-boot-starter-quartz'
        implementation 'com.newrelic.agent.java:newrelic-api:8.11.1'
        implementation 'org.eclipse.jetty:jetty-util:12.0.16'
        implementation 'org.springframework.boot:spring-boot-starter-security'
        implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
        runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
        runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
        implementation 'org.apache.httpcomponents.client5:httpclient5'
        implementation 'commons-jxpath:commons-jxpath:1.3'
        implementation 'com.google.code.gson:gson:2.12.1'
        implementation 'com.codahale.metrics:metrics-core:3.0.2'
        implementation 'com.ryantenney.metrics:metrics-spring:3.1.3'
        implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap'
        annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    
        testImplementation 'org.junit.jupiter:junit-jupiter-api'
        testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
        testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
        testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc"
        testImplementation 'io.rest-assured:rest-assured:5.5.1'
    }
    
    dependencyManagement {
        imports {
            mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
        }
    }
    
    tasks.named('test') {
        outputs.dir snippetsDir // set output directory
        useJUnitPlatform()
    }
    
    tasks.named('asciidoctor') { // add asciidoctor task
        inputs.dir snippetsDir
        dependsOn test
    }
    
    
    1. Update BaseControllerTest:
    package com.test.core.web.rest.controller;
    
    import static org.mockito.ArgumentMatchers.any;
    import static org.mockito.Mockito.when;
    
    import org.apache.commons.validator.routines.UrlValidator;
    import org.junit.jupiter.api.BeforeEach;
    import org.mockito.Mock;
    import org.mockito.MockitoAnnotations;
    import org.springframework.test.context.web.WebAppConfiguration;
    
    @WebAppConfiguration
    public class BaseControllerTest {
    
      // Remove the http:// part and port from here to have it properly documented.
      // Otherwise you will  have smth like: http://http://localhost:8011:8080/account...
      protected static final String MOCK_MVC_HOST = "localhost";
    
      // use port as separate variable
      protected static final int MOCK_MVC_PORT = 8011;
    
      static {
        System.setProperty("org.jboss.logging.provider", "slf4j");
      }
    
      // !!! Remove JUnitRestDocumentation !!!
    
      @Mock
      protected UrlValidator urlValidator;
    
      public BaseControllerTest() {
        MockitoAnnotations.openMocks(this);
      }
    
      @BeforeEach
      public void setupUrlValidatorMock() {
        when(urlValidator.isValid(any())).thenReturn(true);
      }
    
    }
    
    
    1. Update BaseStandaloneControllerTest:
    package com.test.core.web.rest.controller.standalone;
    
    import com.test.core.web.rest.controller.BaseControllerTest;
    import com.test.core.web.rest.controller.ReturnReversalController;
    import com.test.core.web.rest.controller.StandaloneCreditReturnController;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.extension.ExtendWith;
    import org.mockito.InjectMocks;
    import org.mockito.Spy;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.MediaType;
    import org.springframework.mock.web.MockHttpServletRequest;
    import org.springframework.restdocs.RestDocumentationContextProvider;
    import org.springframework.restdocs.RestDocumentationExtension;
    import org.springframework.test.annotation.DirtiesContext;
    import org.springframework.test.web.servlet.MockMvc;
    import org.springframework.web.context.WebApplicationContext;
    
    import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
    import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
    import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
    
    @DirtiesContext
    @ExtendWith(RestDocumentationExtension.class) // add RestDocumentationExtension
    public class BaseStandaloneControllerTest extends BaseControllerTest {
    
      @InjectMocks
      @Spy
      public MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest();
    
      public MockMvc mockMvc;
    
      @Autowired
      public WebApplicationContext webApplicationContext;
    
      HttpHeaders headers = new HttpHeaders();
    
      /*
      *   StandaloneCreditReturnController was changed to ReturnReversalController, because it contains
      *   the /accounts/{accountId}/{returnReversalId} endpoint, which you apparently want to test, whereas
      *   StandaloneCreditReturnController doesn't contain any endpoint.
      *
      *   It was changed only for testing purposes, because StandaloneCreditReturnController is empty
      *
      * */
      @InjectMocks
      ReturnReversalController returnReversalController;
    
    
      // RestDocumentationContextProvider restDocumentation will be automatically instantiated
      @BeforeEach
      public void setUp(RestDocumentationContextProvider restDocumentation) {
        returnReversalController = new ReturnReversalController();
    
        mockMvc = standaloneSetup(returnReversalController)
                .apply(documentationConfiguration(restDocumentation).uris().withHost(MOCK_MVC_HOST).withPort(MOCK_MVC_PORT)
                )
                .build();
      }
    }
    
    
    1. Update StandaloneControllerGetByIdTest:
    package com.test.core.web.rest.controller.standalone;
    
    import org.junit.jupiter.api.Test;
    
    import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
    
    public class StandaloneControllerGetByIdTest
        extends BaseStandaloneControllerTest {
    
      @Test
      public void lookupStandaloneReturnsIsSuccessful() throws Exception {
    
        /*
        * 1. Url was updated to include path variables
        * 2. Added package name, where the report will be added -> .andDo(document("home"))
        *
        * */
        mockMvc.perform(
                        get("/accounts/{accountId}/{returnReversalId}", "123", "04d71b93-79ef-4db4-9c28-e113485ea76d")
                                .contentType(APPLICATION_JSON_VALUE))
                .andDo(print())
                .andDo(document("home")) // add directory to store reports for this test
                .andExpect(status().isOk());
      }
    }
    
    
    1. Add path variables in ReturnReversalController :
    import com.test.core.gateway.domain.GatewayReturnReversalResponse;
    import org.springframework.cloud.context.config.annotation.RefreshScope;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.UUID;
    
    @RefreshScope
    @RestController
    public class ReturnReversalController {
    
      // Added path variables to url
      public static final String RETURN_REVERSAL_GET = "/accounts/{accountId}/{returnReversalId}";
    
      @RequestMapping(method = RequestMethod.GET,
          produces = MediaType.APPLICATION_JSON_VALUE, value = RETURN_REVERSAL_GET)
      @ResponseBody
      @PreAuthorize("hasAuthority('ROLE_USER')")
      public ResponseEntity<GatewayReturnReversalResponse> getReturnReversalById(
          @PathVariable("accountId") String accountId,
          @PathVariable("returnReversalId") UUID returnReversalId) {
          
          GatewayReturnReversalResponse response = new GatewayReturnReversalResponse();
          return new ResponseEntity<>(response, HttpStatus.OK);
        }
    }
    

    After running StandaloneControllerGetByIdTest, you will find a full report in build/generated-snippets/home:

    enter image description here