UnitTestBot / UTBotJava

Automated unit test generation and precise code analysis for Java
Apache License 2.0
137 stars 44 forks source link

Configure fallback from controller-specific integration tests to regular integration tests #2605

Closed IlyaMuravjov closed 1 year ago

IlyaMuravjov commented 1 year ago

Description

Fixes #2557

When generating integration tests for Spring controllers, we don't call method directly, but rather access it via mockMvc.perform(requestBuilder) where via requestBuilder we configure request method, path and other parameters. We do that, because mockMvc closer resembles production environment compared to a direct controller method call, since mockMvc also tests controller method resolving and validates input data the same way it's done in production environment.

However, spring-web is an extremely large module and a huge part of it isn't yet supported in UtBot, which limits our ability to convert direct method call to indirect call via mockMvc.perform(requestBuilder), so if we are unable to do that conversion it's desired to fallback to a regular flow with direct calls.

This PR implements such fallback. Now whenever we generate integration tests for controller method (i.e. when we get non-null result from SpringModelUtils.createRequestBuilderModelOrNull) we:

We fallback to direct calls when we only get failing executions and successful executions with HTTP status code greater or equal to 400, because sometimes even though we technically support all the parameters, we may still be unable to trigger method under test via mockMvc because we are unable to pass some validation (i.e. because [user narrowed the primary mapping with not yet supported headers = ... in their @RequestMapping annotation](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestMapping.html#headers())), see first example in How to test.

We still spend half of the time fuzzing with mockMvc even when we know that we don't support some of the method under test parameters, because oftentimes Spring is able to provide reasonable defaults enabling us to still get some coverage with mockMvc (we prefer executions obtained from mockMvc because they can actually be reproduced in production), see second example in How to test.

How to test

Manual tests

  1. Place the following method in the OwnerController class from spring-petclinic project

    @GetMapping(path = "/owners/find", headers = "content-type=text/*")
    public String initFindForm() {
    return "owners/findOwners";
    }

    Generate integration tests with PetClinicApplication config, there should be tests like this:

    
    @Test
    @DisplayName("initFindForm: ")
    public void testInitFindForm() {
    String actual = ownerController.initFindForm();
    
    String expected = "owners/findOwners";
    
    assertEquals(expected, actual);
    }

@Test @DisplayName("initFindForm: ") public void testInitFindForm1() throws Exception { Object[] uriVariables = {}; MockHttpServletRequestBuilder mockHttpServletRequestBuilder = get("/owners/find", uriVariables);

ResultActions actual = mockMvc.perform(mockHttpServletRequestBuilder);

actual.andDo(print());
actual.andExpect((status()).is(400));
actual.andExpect((content()).string(""));

}

2. Generate integration tests with `PetClinicApplication` config for `OwnerController.processCreationForm()` method from [spring-petclinic](https://github.com/spring-projects/spring-petclinic/tree/main/src/main) project, there should be tests like this:
```java
@Test
@DisplayName("processCreationForm: owner = Owner(), result = null")
public void testProcessCreationForm() throws Exception {
    Object[] uriVariables = {};
    MockHttpServletRequestBuilder mockHttpServletRequestBuilder = post("/owners/new", uriVariables);

    ResultActions actual = mockMvc.perform(mockHttpServletRequestBuilder);

    actual.andDo(print());
    actual.andExpect((status()).is(200));
    actual.andExpect((view()).name("owners/createOrUpdateOwnerForm"));
    actual.andExpect((content()).string("<html>\r\n\r\n<head>\r\n\r\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\r\n  <meta charset=\"utf-8\">\r\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\r\n\r\n  <link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/resources/images/favicon.png\">\r\n\r\n  <title>PetClinic :: a Spring Framework demonstration</title>\r\n\r\n  <!--[if lt IE 9]>\r\n    <script src=\"https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js\"></script>\r\n    <script src=\"https://oss.maxcdn.com/respond/1.4.2/respond.min.js\"></script>\r\n    <![endif]-->\r\n\r\n  <link href=\"/webjars/font-awesome/4.7.0/css/font-awesome.min.css\" rel=\"stylesheet\">\r\n  <link rel=\"stylesheet\" href=\"/resources/css/petclinic.css\" />\r\n\r\n</head>\r\n\r\n<body>\r\n\r\n  <nav class=\"navbar navbar-expand-lg navbar-dark\" role=\"navigation\">\r\n    <div class=\"container-fluid\">\r\n      <a class=\"navbar-brand\" href=\"/\"><span></span></a>\r\n      <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#main-navbar\">\r\n        <span class=\"navbar-toggler-icon\"></span>\r\n      </button>\r\n      <div class=\"collapse navbar-collapse\" id=\"main-navbar\" style>\r\n\r\n        \r\n\r\n        <ul class=\"nav navbar-nav me-auto\">\r\n\r\n          <li class=\"nav-item\">\r\n            <a class=\"nav-link\" href=\"/\" title=\"home page\">\r\n              <span class=\"fa fa-home\"></span>\r\n              <span>Home</span>\r\n            </a>\r\n          </li>\r\n\r\n          <li class=\"nav-item\">\r\n            <a class=\"nav-link active\" href=\"/owners/find\" title=\"find owners\">\r\n              <span class=\"fa fa-search\"></span>\r\n              <span>Find owners</span>\r\n            </a>\r\n          </li>\r\n\r\n          <li class=\"nav-item\">\r\n            <a class=\"nav-link\" href=\"/vets.html\" title=\"veterinarians\">\r\n              <span class=\"fa fa-th-list\"></span>\r\n              <span>Veterinarians</span>\r\n            </a>\r\n          </li>\r\n\r\n          <li class=\"nav-item\">\r\n            <a class=\"nav-link\" href=\"/oups\" title=\"trigger a RuntimeException to see how it is handled\">\r\n              <span class=\"fa fa-exclamation-triangle\"></span>\r\n              <span>Error</span>\r\n            </a>\r\n          </li>\r\n\r\n        </ul>\r\n      </div>\r\n    </div>\r\n  </nav>\r\n  <div class=\"container-fluid\">\r\n    <div class=\"container xd-container\">\r\n\r\n      <body>\r\n\r\n  <h2>Owner</h2>\r\n  <form class=\"form-horizontal\" id=\"add-owner-form\" method=\"post\">\r\n    <div class=\"form-group has-feedback\">\r\n      \r\n      <div class=\"form-group has-error\">\r\n        <label class=\"col-sm-2 control-label\">First Name</label>\r\n        <div class=\"col-sm-10\">\r\n            <div>\r\n                <input class=\"form-control\" type=\"text\" id=\"firstName\" name=\"firstName\" value=\"\" />\r\n                \r\n            </div>\r\n          \r\n          \r\n            <span\r\n              class=\"fa fa-remove form-control-feedback\"\r\n              aria-hidden=\"true\"></span>\r\n            <span class=\"help-inline\">must not be empty</span>\r\n          \r\n        </div>\r\n      </div>\r\n    \r\n      \r\n      <div class=\"form-group has-error\">\r\n        <label class=\"col-sm-2 control-label\">Last Name</label>\r\n        <div class=\"col-sm-10\">\r\n            <div>\r\n                <input class=\"form-control\" type=\"text\" id=\"lastName\" name=\"lastName\" value=\"\" />\r\n                \r\n            </div>\r\n          \r\n          \r\n            <span\r\n              class=\"fa fa-remove form-control-feedback\"\r\n              aria-hidden=\"true\"></span>\r\n            <span class=\"help-inline\">must not be empty</span>\r\n          \r\n        </div>\r\n      </div>\r\n    \r\n      \r\n      <div class=\"form-group has-error\">\r\n        <label class=\"col-sm-2 control-label\">Address</label>\r\n        <div class=\"col-sm-10\">\r\n            <div>\r\n                <input class=\"form-control\" type=\"text\" id=\"address\" name=\"address\" value=\"\" />\r\n                \r\n            </div>\r\n          \r\n          \r\n            <span\r\n              class=\"fa fa-remove form-control-feedback\"\r\n              aria-hidden=\"true\"></span>\r\n            <span class=\"help-inline\">must not be empty</span>\r\n          \r\n        </div>\r\n      </div>\r\n    \r\n      \r\n      <div class=\"form-group has-error\">\r\n        <label class=\"col-sm-2 control-label\">City</label>\r\n        <div class=\"col-sm-10\">\r\n            <div>\r\n                <input class=\"form-control\" type=\"text\" id=\"city\" name=\"city\" value=\"\" />\r\n                \r\n            </div>\r\n          \r\n          \r\n            <span\r\n              class=\"fa fa-remove form-control-feedback\"\r\n              aria-hidden=\"true\"></span>\r\n            <span class=\"help-inline\">must not be empty</span>\r\n          \r\n        </div>\r\n      </div>\r\n    \r\n      \r\n      <div class=\"form-group has-error\">\r\n        <label class=\"col-sm-2 control-label\">Telephone</label>\r\n        <div class=\"col-sm-10\">\r\n            <div>\r\n                <input class=\"form-control\" type=\"text\" id=\"telephone\" name=\"telephone\" value=\"\" />\r\n                \r\n            </div>\r\n          \r\n          \r\n            <span\r\n              class=\"fa fa-remove form-control-feedback\"\r\n              aria-hidden=\"true\"></span>\r\n            <span class=\"help-inline\">must not be empty</span>\r\n          \r\n        </div>\r\n      </div>\r\n    \r\n    </div>\r\n    <div class=\"form-group\">\r\n      <div class=\"col-sm-offset-2 col-sm-10\">\r\n        <button\r\n          class=\"btn btn-primary\" type=\"submit\">Add Owner</button>\r\n      </div>\r\n    </div>\r\n  </form>\r\n</body>\r\n\r\n      <br />\r\n      <br />\r\n      <div class=\"container\">\r\n        <div class=\"row\">\r\n          <div class=\"col-12 text-center\">\r\n            <img src=\"/resources/images/spring-logo.svg\" alt=\"VMware Tanzu Logo\" class=\"logo\">\r\n          </div>\r\n        </div>\r\n      </div>\r\n    </div>\r\n  </div>\r\n\r\n  <script src=\"/webjars/bootstrap/5.2.3/dist/js/bootstrap.bundle.min.js\"></script>\r\n\r\n</body>\r\n\r\n</html>\r\n"));
}

@Test
@DisplayName("processCreationForm: owner = Owner(), result = BindException(Object, String)")
public void testProcessCreationForm1() {
    Owner owner = new Owner();
    owner.setTelephone("-3");
    owner.setFirstName("XZ");
    owner.setLastName("#$\\\"'");
    owner.setAddress(" ");
    owner.setCity("1@0");
    Object target = new Object();
    BindException result = new BindException(target, "\n\t\r");
    StackTraceElement[] stackTraceElementArray = new StackTraceElement[2];
    StackTraceElement stackTraceElement = new StackTraceElement("abc", "XZ", "10", Integer.MAX_VALUE);
    stackTraceElementArray[0] = stackTraceElement;
    StackTraceElement stackTraceElement1 = new StackTraceElement("\n\t\r", "\n\t\r", "#$\\\"'", "\n\t\r", "", "", 0);
    stackTraceElementArray[1] = stackTraceElement1;
    result.setStackTrace(stackTraceElementArray);

    String actual = ownerController.processCreationForm(owner, result);

    String expected = "redirect:/owners/1";

    assertEquals(expected, actual);
}

Self-check list