Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 19, 2023

Conversation

IlyaMuravjov
Copy link
Collaborator

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:

  • spend first half of the fuzzing time trying to use mockMvc
  • spend the second half of the fuzzing time using mockMvc iff we do support all parameters of method under test and during the first half of the fuzzing time we were able to get an execution that didn't throw an exception and had an HTTP status code smaller than 400
  • otherwise spend the second half of the fuzzing time using direct calls
  • prior to minimization remove UtExecutions that use direct call and have the same coverage of method under test and same successfulness as some other UtExecution that uses mockMvc (only coverage inside method under test is considered because when mockMvc is used Spring may execute additional code before/after method under test which will cause coverage differ from the one obtained from direct call)

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), 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(""));
}
  1. Generate integration tests with PetClinicApplication config for OwnerController.processCreationForm() method from spring-petclinic project, there should be tests like this:
@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

  • I've set the proper labels for my PR (at least, for category and component).
  • PR title and description are clear and intelligible.
  • I've added enough comments to my code, particularly in hard-to-understand areas.
  • The functionality I've repaired, changed or added is covered with automated tests.
  • Manual tests have been provided optionally.
  • The documentation for the functionality I've been working on is up-to-date.

@IlyaMuravjov IlyaMuravjov added ctg-enhancement New feature, improvement or change request comp-fuzzing Issue is related to the fuzzing labels Sep 18, 2023
@IlyaMuravjov IlyaMuravjov force-pushed the ilya_m/spring-controller-fallback branch from 9f35623 to 6620fc0 Compare September 18, 2023 17:18
@IlyaMuravjov IlyaMuravjov force-pushed the ilya_m/spring-controller-fallback branch from 6620fc0 to 45d8585 Compare September 18, 2023 17:28
@EgorkaKulikov EgorkaKulikov merged commit dd787d0 into main Sep 19, 2023
@EgorkaKulikov EgorkaKulikov deleted the ilya_m/spring-controller-fallback branch September 19, 2023 09:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
comp-fuzzing Issue is related to the fuzzing ctg-enhancement New feature, improvement or change request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Fallback from controller-specific integration tests to general purpose integration tests
2 participants