0

Feeling very stupid, but I'm not able to test an endpoint in Spring Boot (version 2.7.1) with JUnit 5.

Briefly, I want to test the real endpoint response, so I've created a test class like explained in Testing the Web Layer. Herein the code:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ApiDocumentationControllerIntegrationTest {

  @Autowired
  private TestRestTemplate restTemplate;

  @Test
  public void getOpenApiDocumentationShouldReturnOk() {
    assertThat(restTemplate.getForEntity("/api-docs", String.class).getStatusCode())
      .isEqualTo(HttpStatus.OK);
  }
}

But when I run the test, TestRestTemplate calls http://localhost:8080/api-docs ignoring the fact that server should be listen to a random port.

What am I missing? As other examples suggest, I've tried to add:

  @LocalServerPort
  private int randomServerPort;

But in this case I have an exception during the launch of the test:

java.lang.IllegalArgumentException: Could not resolve placeholder 'local.server.port' in value "${local.server.port}"

I've tried to set 0 as port —that should be considered a random port by the framework— with no success. Spring complains it cannot listen localhost:0.

Service configuration is empty (AKA application.yaml is empty and I didn't set params in other ways), so all the configuration values are the default ones by Spring Boot.

Probably it is a problem for dummies but since yesterday I'm looking for a solution but I didn't find it.

More details

Controller

@Slf4j
@Controller
public class ApiDocumentationController {

  private final Resource resourceFile;
  private final ObjectMapper yamlReader;
  private final ObjectMapper jsonWriter;

  public ApiDocumentationController(@Value("classpath:openapi/api-documentation.yaml") Resource resourceFile,
                                    @Qualifier("yamlReader") ObjectMapper yamlReader,
                                    ObjectMapper objectMapper) {
    this.resourceFile = resourceFile;
    this.yamlReader = yamlReader;
    this.jsonWriter = objectMapper;
  }

  @GetMapping(value = "/api-docs", produces = {MediaType.APPLICATION_JSON_VALUE})
  public ResponseEntity<String> getOpenApiDocumentation() {
    return Try.of(() -> yamlReader.readValue(resourceFile.getInputStream(), Object.class))
      .mapTry(jsonWriter::writeValueAsString)
      .map(apiDocumentation -> ResponseEntity.status(HttpStatus.OK).body(apiDocumentation))
      .get();  // FIXME This forced Try::get is ugly
  }
}

Launcher

@SpringBootApplication
public class AirportTravellersInsightsServiceApplication {

  public static void main(String[] args) {
    SpringApplication.run(AirportTravellersInsightsServiceApplication.class, args);
  }

Gradle build

(This is an excerpt of build.gradle.)

Test mentioned above is in integrationTest source set.

plugins {
  id 'org.springframework.boot' version '2.7.1'
  id 'io.spring.dependency-management' version '1.0.11.RELEASE'
  id 'java'
  id 'jacoco'
  id 'checkstyle'
  id 'idea'
}

group = 'example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
  mavenCentral()
}

ext.versions = [
  checkstyleVersion: "8.39",
  vavrVersion:       "0.10.4"
]

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  implementation 'org.projectlombok:lombok'
  annotationProcessor 'org.projectlombok:lombok'
  implementation "io.vavr:vavr:${versions.vavrVersion}"
  implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml'
  // TEST
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
  testImplementation 'org.springframework.security:spring-security-test'
  testImplementation 'org.junit.jupiter:junit-jupiter-api'
  testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine"
  testAnnotationProcessor 'org.projectlombok:lombok'
}

sourceSets {
  integrationTest {
    compileClasspath += sourceSets.main.output
    compileClasspath += sourceSets.test.output
    runtimeClasspath += sourceSets.main.output
    runtimeClasspath += sourceSets.test.output
  }
}

configurations {
  compileOnly {
    extendsFrom annotationProcessor
  }
  integrationTestImplementation.extendsFrom testImplementation
  integrationTestRuntimeOnly.extendsFrom runtimeOnly
  implementation {
    exclude module: 'spring-boot-starter-tomcat'
  }
}

tasks.named('test') {
  useJUnitPlatform()
}

task integrationTest(type: Test, description: 'Runs integration tests.', group: LifecycleBasePlugin.VERIFICATION_GROUP) {
  useJUnitPlatform()
  testClassesDirs = sourceSets.integrationTest.output.classesDirs
  classpath = sourceSets.integrationTest.runtimeClasspath
  shouldRunAfter test
}
check.dependsOn integrationTest

Gilberto T.
  • 358
  • 6
  • 19
  • What is the version of spring-boot? Can you check if `TestRestTemplate` explicitly configured with rootUri somewhere in your project? Is port 8080 explicitly configured somewhere in your project? If yes, can you share that configuration? – Shivaji Pote Jul 03 '22 at 10:14
  • Spring Boot version is `2.7.1` (added to the question) and I did not configured `TestRestTemplate` in other parts of the projects. Regarding the port number, I didn't configured it and `application.yaml` is empty at the moment. – Gilberto T. Jul 03 '22 at 10:19
  • Is it possible for you to share minimum reproducible example? With same spring-boot version and no other configurations, it's launching tomcat on random port for me – Shivaji Pote Jul 03 '22 at 10:30
  • @ShivajiPote I've added to my post details on the controller and Gradle build file open that it may help. – Gilberto T. Jul 03 '22 at 10:41
  • 1
    What I meant is push your changes to some public (like GitHub) repository where we could reproduce problem and try to fix it. BTW how do you know it's launching server on 8080? Is it logged somewhere? like this `2022-07-03 12:48:19.721 INFO 8135 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 54799 (http) with context path ''` – Shivaji Pote Jul 03 '22 at 10:51
  • @ShivajiPote You've right. I don't know for what reason the service didn't start, so I assume that `@Autowired` worked for a short time. I moved to previous commit and started to introduce the test again an now it works. – Gilberto T. Jul 03 '22 at 12:12

1 Answers1

0

I propose you to use org.springframework.test.web.servlet.MockMvc for testing your endpoints. You can read this to see why you should use MockMvc.

MockMvc provides support for Spring MVC testing. It encapsulates all web application beans and makes them available for testing.

You can perform your test this way:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class ApiDocumentationControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void getOpenApiDocumentationShouldReturnOk() {
        // Act
        ResultActions response = mockMvc.perform(//
                get("/api-docs"));

        // Assert
        response.andDo(print()).//
                andExpect(status().isOk())//
    }
}

Read more about testing with MockMvc here and here.

UPDATE

There is another approach to initiate the MockMvc instance in your test class:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ApiDocumentationControllerIntegrationTest {

    @Autowired
    private WebApplicationContext webApplicationContext;
    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

}
  • Thanks for the suggestion. I've tried and now I have to fix error `Unsatisfied dependency expressed through field 'mockMvc'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.test.web.servlet.MockMvc' available`. I'm reading the documentation to check if I've missed something. – Gilberto T. Jul 03 '22 at 08:35
  • Have you added `@AutoConfigureMockMvc` to your test class? – Soroush Shemshadi Jul 03 '22 at 08:55
  • And have you included `testImplementation 'org.springframework.boot:spring-boot-starter-web'` in your dependencies? Or ` org.springframework.security spring-security-test test ` for maven. – Soroush Shemshadi Jul 03 '22 at 09:08
  • `org.springframework.boot:spring-boot-starter-web` is there. Regarding `spring-security-test` the project doesn't use Spring Security. – Gilberto T. Jul 03 '22 at 09:13
  • Ok, there is another approach to using `MockMvc`. I'll update my answer and add it. Please try it out I hope it will solve your problem – Soroush Shemshadi Jul 03 '22 at 09:17
  • Mh... Other error: `Unsatisfied dependency expressed through field 'webApplicationContext'`. I think that `@Autowired` is not working properly. – Gilberto T. Jul 03 '22 at 09:30
  • I faced these errors before, but the problem was that I forgot to add `@SpringBootTest` to my class. In your case, I don't know what may go wrong unless I could see your source code. If your project lies on an SCM like GitHub or GitLab and you don't have security issues, please send the link of it to my email `shuoros@yahoo.com`. and I'll check it – Soroush Shemshadi Jul 03 '22 at 10:40
  • I've added more details to my original message. Thanks for help. – Gilberto T. Jul 03 '22 at 10:43
  • I'm not sure if it's the solution, but it is worth trying. Please consider adding `@ContextConfiguration(classes = DefaultTestConfiguration.class)` to my updated solution and see if problem still exist or not. – Soroush Shemshadi Jul 03 '22 at 11:18