I will be straightforward, honestly not sure where to post this exactly but this was probably the weirdest bug we ever had. A few lines of code say more than a thousand words: This is from a university project where we develop a web application for students to track exams and vacation time during a specific timeframe. Its shortened to relevant parts.

@Controller
@Secured("ROLE_STUDENT")
public class StudentController {
  private final ApplicationService applicationService;
  public StudentController(ApplicationService applicationService) {
    this.applicationService = applicationService;
  }

  @PostMapping("/klausuren")
  public String insertKlausur(KlausurInputDTO klausur) {
    applicationService.insertKlausur(klausur);
    return "redirect:/klausuranmeldung";
  }
}
  ````
This works just fine in the normal runtime environment outside of testing. The KlausurInputDTO is simply a record with a few attributes which will be infered and then injected through the spring framework from the POST request payload.
Testing this method results in the following code (again shortened to relevant bits):
```java
@WebMvcTest
@AutoConfigureMockMvc
public class StudentControllerTest {
  @Autowired
  MockMvc mvc;
  @MockBean
  ApplicationService appService;

  @Test
  @WithMockUser("Test")
  @DisplayName("Controller calls insertKlausur")
  void test_7() throws Exception {
    KlausurInputDTO klausurInputDTO = KlausurInputDTOTestData.build(K123_PRAESENZ_12_30_TO_14_30);
    RequestBuilder builder = MockMvcRequestBuilders.post("/klausuren")
        .flashAttr("klausur", klausurInputDTO)
        .with(csrf());
    mvc.perform(builder);
    verify(appService, times(1)).insertKlausur(klausurInputDTO);
  }
}

The test results in an error 400 - Bad request. The bug in this code exists in the naming of the KlausurInputDTO object, during a test environment we somehow need to have the name of the object be the same as its datatype. therefore the fixed code would be (relevant parts only):

  @PostMapping("/klausuren")
  public String insertKlausur(KlausurInputDTO klausurInputDTO) {
    applicationService.insertKlausur(klausurInputDTO);
    return "redirect:/klausuranmeldung";
  }
 ```
and 
```java
  void test_7() throws Exception {
    KlausurInputDTO klausurInputDTO = KlausurInputDTOTestData.build(K123_PRAESENZ_12_30_TO_14_30);
    RequestBuilder builder = MockMvcRequestBuilders.post("/klausuren")
        .flashAttr("klausurInputDTO", klausurInputDTO)
        .with(csrf());
    mvc.perform(builder);
    verify(appService, times(1)).insertKlausur(klausurInputDTO);
  }

this binds the naming of the object incredibly tightly and makes for quite an obscure bug.

This issue only arises while using the testing environment and not when actually running the application normally. lastly here is the build.gradle with the dependency stuff (once again shortened):

plugins {
    id 'org.springframework.boot' version '2.6.4'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
    id "com.github.spotbugs" version "4.7.9"
    id "checkstyle"
}



dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.flywaydb:flyway-core'
    implementation 'org.springframework.session:spring-session-core'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
    implementation 'com.github.hazendaz.jsoup:jsoup:1.15.1'
    implementation 'com.google.guava:guava:31.1-jre'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
    runtimeOnly 'com.h2database:h2'
    compileOnly 'com.github.spotbugs:spotbugs-annotations:4.6.0'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
    testImplementation 'com.tngtech.archunit:archunit-junit5-engine:0.22.0'
}

Comment From: bclozel

@Daniel-Mueller96 it looks like you have all the pieces available to craft a minimal repro for this issue. Could you remove as much as possible (code and dependencies) and share a project we can git clone or download to reproduce the issue?

Comment From: Daniel-Mueller96

@bclozel I can try that. Never really did this though. Therefore can not say that for sure

Comment From: scottfrederick

@Daniel-Mueller96 You can create a zip file from the minimal sample application and attach the zip in a comment on this issue if that's easier than creating a git repository.

Comment From: Daniel-Mueller96

@bclozel @scottfrederick https://github.com/Daniel-Mueller96/Spring-boot-issue-30258 This is the repo to clone. I made it as simple as i had the time for (its getting late here too) There are two controllers, one is fixed and the other isnt. There are also two tests, one is fixed and the other isnt. Both test intentionally fail to show you the error and provide you with the 400 or 302 depending on the controller version.

edit: i should say that i did not test wether this now works running the application normally.

Comment From: Daniel-Mueller96

I just updated the repo mentioned above with a clearer version and now also tested that this still works running normally by entering form components manually and sending them. The value of the object simply gets printed on stdout. I also made the tests actually check for the error and not just fail.

Comment From: bclozel

Thanks for the repro project, that was really useful.

The main difference between the two controllers are indeed the method signature:

// works
public String insertKlausur(KlausurInputDTO klausurInputDTO) {

// doesn't work
public String insertKlausur(KlausurInputDTO klausur) {

This happens because in the first instance, the argument is resolved from the ModelAndView using the parameter name. In your test setup the KlausurInputDTO instance is contributed to the model with its type name (first letter lowercased): klausurInputDTO. This explains why in the first case it's found and not the other one.

The core problem here is that your test setup is invalid. The usage of flashAttr is odd here, as you're not testing a flash attribute that's been contributed by another controller handler before a redirect. You should be testing for a real HTML form input; this is after all the goal of a MockMvc test: checking that the HTTP behavior is right.

The following changes work:

  @Test
  void test_7() throws Exception {
        RequestBuilder builder = MockMvcRequestBuilders.post("/klausuren")
            .param("lsfId", "1")
            .param("date", "2022-02-02")
            .param("startTime", "08:30")
            .param("endTime", "12:30")
            .param("present", "true")
            .param("name", "test");
    mvc.perform(builder).andExpect(status().is(302));
  }
public record KlausurInputDTO(Long lsfId, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date,
    @DateTimeFormat(pattern = "HH:mm") LocalTime startTime,
    @DateTimeFormat(pattern = "HH:mm") LocalTime endTime, 
    boolean present, String name) {

}

I'm closing this issue as this works as designed.

Comment From: Daniel-Mueller96

Okay thanks for checking though