|
Spring Framework is a well-known Java framework to build Java-based applications, supporting IoC (Inversion of Control) principle.
Sprint Boot provides an opinionated extension on top of Spring that aims to minimize configuration burden and ease the implementation of applications.
With Spring it's possible to create web applications, REST services, and more.
For this example we will use the built-in testing facilities provided by Spring to test the application developed in Spring Boot We will need:
|
To start using Spring Boot please follow the Quick Start Guide documentation; you can also use Spring initializr to make a working skeleton of a project using Spring and its dependencies.
Usually, Spring applications have these layers:
The target SUT is a web application implemented using Spring Boot, having a REST API to manage users and some controllers that return text acting like typical servlets.
Our Spring application provides:
We can run our Spring application from the command line.
mvn spring-boot:run |
We'll implement JUnit 5 tests for all these layers:
Spring Boot supports test slicing; the idea is to provide slices of the whole ApplicationContext by loading fewer components, thus providing efficiency. We'll see more about @DataJpaTest and @WebMvcTest ahead. |
To test at the data layer, we can use @DataJpaTest as a test slice to test our UserRepository repository and the User entity.
To test at the service layer, we won't need to boot the application; we can perform unit tests and mock dependency on the data layer.
To test at web layer we can follow different approaches by annotating the related test classes:
@SpringBootTest
@SpringBootTest
(webEnvironment = WebEnvironment.MOCK, classes = ...) + @AutoConfigureMockMvc@WebMvcTest(xxx.class)
@WebMvcTest
is used in combination with @MockBean
or @Import
to create any collaborators required by your @Controller
beans.Let's see some examples, precisely focused more on the web layer.
The following code snippet shows usage of @WebMvcTest to test a slice containing just the web layer. In this case we're testing the output of the root page.
Even though we don't have to use them, we'll also take advantage of 2 annotations provided by the xray-junit-extensions maven plugin to showcase additional features:
package com.idera.xray.tutorials.springboot; import static org.hamcrest.Matchers.equalTo; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import com.idera.xray.tutorials.springboot.boundary.IndexController; // @SpringBootTest // @AutoConfigureMockMvc; it is implied whenever @WebMvcTest is used // @WebMvcTest annotation is used to test only the web layer of the application // It disables full auto-configuration and instead apply only configuration relevant to MVC tests @WebMvcTest(IndexController.class) public class IndexControllerMockedIT { @Autowired private MockMvc mvc; @Test @XrayTest(key = "XT-676") @Requirement("XT-675") public void getWelcomeMessage() throws Exception { mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.TEXT_PLAIN)) .andExpect(status().isOk()) .andExpect(content().string(equalTo("Welcome to this amazing website!"))); } } |
The following code snippet loads the whole application using @SpringBootTest to test the REST API endpoints used to manage users.
package com.idera.xray.tutorials.springboot; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.util.UriComponentsBuilder; import org.springframework.boot.test.web.server.LocalServerPort; import com.idera.xray.tutorials.springboot.data.User; import com.idera.xray.tutorials.springboot.data.UserRepository; import app.getxray.xray.junit.customjunitxml.annotations.XrayTest; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; /* @SpringBootTest loads the full application, including the web server * @AutoConfigureTestDatabase is used to configure a test database instead of the application-defined database */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @AutoConfigureTestDatabase class UserRestControllerIT { @LocalServerPort int randomServerPort; @Autowired private TestRestTemplate restTemplate; @Autowired private UserRepository repository; User user1; @BeforeEach public void resetDb() { repository.deleteAll(); user1 = repository.save(new User("Sergio Freire", "sergiofreire", "dummypassword")); } @Test void createUserWithSuccess() { User john = new User("John Doe", "johndoe", "dummypassword"); ResponseEntity<User> entity = restTemplate.postForEntity("/api/users", john, User.class); List<User> foundUsers = repository.findAll(); assertThat(foundUsers).extracting(User::getUsername).contains("johndoe"); } @Test void dontCreateUserForInvalidData() { User john = new User("John Doe", "", "dummypassword"); ResponseEntity<User> response = restTemplate.postForEntity("/api/users", john, User.class); // ideally, the server shouldnt return 500, but 400 (bad request) assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); List<User> found = repository.findAll(); assertThat(found).hasSize(1); assertThat(found).extracting(User::getName).doesNotContain("John Doe"); } @Test void getUserWithSuccess() { String endpoint = UriComponentsBuilder.newInstance() .scheme("http") .host("127.0.0.1") .port(randomServerPort) .pathSegment("api", "users", user1.getId().toString() ) .build() .toUriString(); ResponseEntity<User> response = restTemplate.exchange(endpoint, HttpMethod.GET, null, new ParameterizedTypeReference<User>() { }); User user = response.getBody(); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(user1.equals(user)).isTrue(); } @Test void getUserUnsuccess() throws JSONException { /* String endpoint = UriComponentsBuilder.newInstance() .scheme("http") .host("127.0.0.1") .port(randomServerPort) .pathSegment("api", "users", "-1" ) .build() .toUriString(); */ ResponseEntity<JSONObject> response = restTemplate.exchange("/api/user/-1", HttpMethod.GET, null, new ParameterizedTypeReference<JSONObject>() { }); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); } @Test void listAllUsersWithSuccess() { createTempUser("Amanda James", "amanda", "dummypassword"); createTempUser("Robert Junior", "robert", "dummypassword"); ResponseEntity<List<User>> response = restTemplate .exchange("/api/users", HttpMethod.GET, null, new ParameterizedTypeReference<List<User>>() { }); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).extracting(User::getName).containsExactly("Sergio Freire", "Amanda James", "Robert Junior"); } @Test void deleteUserWithSuccess() { ResponseEntity<User> response = restTemplate.exchange("/api/users/" + user1.getId(), HttpMethod.DELETE, null, User.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody().getName()).isEqualTo("Sergio Freire"); List<User> found = repository.findAll(); assertThat(found).isEmpty(); } @Test void deleteUserUnsuccess() { ResponseEntity<User> response = restTemplate.exchange("/api/users/" + (user1.getId()+2), HttpMethod.DELETE, null, User.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); List<User> found = repository.findAll(); assertThat(found).hasSize(1); } private void createTempUser(String name, String username, String password) { User user = new User(name, username, password); repository.saveAndFlush(user); } } |
In our case, we have tests that will be picked by surefire plugin and other ones that will be picked by failsafe plugin.
Once the code is implemented it can be executed with the following command:
mvn test failsafe:integration-test |
The results are immediately available in the terminal.
Ultimately this will lead to multiple JUnit XML reports (in target/surefire-reports/
and target/failsafe-reports/
, respectively).
If we use the xray-junit-extensions maven plugin, it will generate 1 JUnit XML report (i.e., in reports/TEST-junit-jupiter.xml
) with all results of the last task executed (i.e., the integration tests ran by failsafe on the previous mvn command).
In this example, all tests have succeeded, as seen in the previous terminal screenshot. It generates the following JUnit XML report.
<?xml version="1.0" encoding="UTF-8"?> <testsuite name="JUnit Jupiter" tests="11" skipped="0" failures="0" errors="0" time="6" hostname="Sergios-MBP.lan" timestamp="2024-02-20T18:15:39"> <properties> <property name="CONSOLE_LOG_CHARSET" value="UTF-8" /> <property name="FILE_LOG_CHARSET" value="UTF-8" /> <property name="user.country" value="PT" /> <property name="user.timezone" value="Europe/Lisbon" /> </properties> <testcase name="getPersonalizedGreeting" classname="com.idera.xray.tutorials.springboot.GreetingControllerMockedIT" time="0" started-at="2024-02-20T18:15:37.51681" finished-at="2024-02-20T18:15:37.814122"> <system-out><![CDATA[ unique-id: [engine:junit-jupiter]/[class:com.idera.xray.tutorials.springboot.GreetingControllerMockedIT]/[method:getPersonalizedGreeting()] display-name: getPersonalizedGreeting() ]]></system-out> <properties> <property name="_dummy_" value="" /> </properties> </testcase> <testcase name="dontCreateUserForInvalidData" classname="com.idera.xray.tutorials.springboot.UserRestControllerIT" time="0" started-at="2024-02-20T18:15:38.375802" finished-at="2024-02-20T18:15:38.766131"> <system-out><![CDATA[ unique-id: [engine:junit-jupiter]/[class:com.idera.xray.tutorials.springboot.UserRestControllerIT]/[method:dontCreateUserForInvalidData()] display-name: dontCreateUserForInvalidData() ]]></system-out> <properties> <property name="_dummy_" value="" /> </properties> </testcase> <testcase name="getUserUnsuccess" classname="com.idera.xray.tutorials.springboot.UserRestControllerIT" time="0" started-at="2024-02-20T18:15:38.845142" finished-at="2024-02-20T18:15:38.868507"> <system-out><![CDATA[ unique-id: [engine:junit-jupiter]/[class:com.idera.xray.tutorials.springboot.UserRestControllerIT]/[method:getUserUnsuccess()] display-name: getUserUnsuccess() ]]></system-out> <properties> <property name="_dummy_" value="" /> </properties> </testcase> <testcase name="deleteUserWithSuccess" classname="com.idera.xray.tutorials.springboot.UserRestControllerIT" time="0" started-at="2024-02-20T18:15:38.804222" finished-at="2024-02-20T18:15:38.824641"> <system-out><![CDATA[ unique-id: [engine:junit-jupiter]/[class:com.idera.xray.tutorials.springboot.UserRestControllerIT]/[method:deleteUserWithSuccess()] display-name: deleteUserWithSuccess() ]]></system-out> <properties> <property name="_dummy_" value="" /> </properties> </testcase> <testcase name="createUserWithSuccess" classname="com.idera.xray.tutorials.springboot.UserRestControllerIT" time="0" started-at="2024-02-20T18:15:38.825412" finished-at="2024-02-20T18:15:38.844362"> <system-out><![CDATA[ unique-id: [engine:junit-jupiter]/[class:com.idera.xray.tutorials.springboot.UserRestControllerIT]/[method:createUserWithSuccess()] display-name: createUserWithSuccess() ]]></system-out> <properties> <property name="_dummy_" value="" /> </properties> </testcase> <testcase name="getDefaultGreeting" classname="com.idera.xray.tutorials.springboot.GreetingControllerMockedIT" time="0" started-at="2024-02-20T18:15:37.814867" finished-at="2024-02-20T18:15:37.818444"> <system-out><![CDATA[ unique-id: [engine:junit-jupiter]/[class:com.idera.xray.tutorials.springboot.GreetingControllerMockedIT]/[method:getDefaultGreeting()] display-name: getDefaultGreeting() ]]></system-out> <properties> <property name="_dummy_" value="" /> </properties> </testcase> <testcase name="getWelcomeMessage" classname="com.idera.xray.tutorials.springboot.IndexControllerMockedIT" time="0" started-at="2024-02-20T18:15:39.091011" finished-at="2024-02-20T18:15:39.097803"> <system-out><![CDATA[ unique-id: [engine:junit-jupiter]/[class:com.idera.xray.tutorials.springboot.IndexControllerMockedIT]/[method:getWelcomeMessage()] display-name: getWelcomeMessage() ]]></system-out> <properties> <property name="requirements" value="XT-675" /> <property name="test_key" value="XT-676" /> <property name="_dummy_" value="" /> </properties> </testcase> <testcase name="getUserWithSuccess" classname="com.idera.xray.tutorials.springboot.UserRestControllerIT" time="0" started-at="2024-02-20T18:15:38.767197" finished-at="2024-02-20T18:15:38.803133"> <system-out><![CDATA[ unique-id: [engine:junit-jupiter]/[class:com.idera.xray.tutorials.springboot.UserRestControllerIT]/[method:getUserWithSuccess()] display-name: getUserWithSuccess() ]]></system-out> <properties> <property name="_dummy_" value="" /> </properties> </testcase> <testcase name="listAllUsersWithSuccess" classname="com.idera.xray.tutorials.springboot.UserRestControllerIT" time="0" started-at="2024-02-20T18:15:38.888027" finished-at="2024-02-20T18:15:38.909762"> <system-out><![CDATA[ unique-id: [engine:junit-jupiter]/[class:com.idera.xray.tutorials.springboot.UserRestControllerIT]/[method:listAllUsersWithSuccess()] display-name: listAllUsersWithSuccess() ]]></system-out> <properties> <property name="_dummy_" value="" /> </properties> </testcase> <testcase name="getWelcomeMessage" classname="com.idera.xray.tutorials.springboot.IndexControllerIT" time="0" started-at="2024-02-20T18:15:36.488501" finished-at="2024-02-20T18:15:37.25418"> <system-out><![CDATA[ unique-id: [engine:junit-jupiter]/[class:com.idera.xray.tutorials.springboot.IndexControllerIT]/[method:getWelcomeMessage()] display-name: getWelcomeMessage() ]]></system-out> <properties> <property name="_dummy_" value="" /> </properties> </testcase> <testcase name="deleteUserUnsuccess" classname="com.idera.xray.tutorials.springboot.UserRestControllerIT" time="0" started-at="2024-02-20T18:15:38.869301" finished-at="2024-02-20T18:15:38.887141"> <system-out><![CDATA[ unique-id: [engine:junit-jupiter]/[class:com.idera.xray.tutorials.springboot.UserRestControllerIT]/[method:deleteUserUnsuccess()] display-name: deleteUserUnsuccess() ]]></system-out> <properties> <property name="_dummy_" value="" /> </properties> </testcase> <system-out><![CDATA[ unique-id: [engine:junit-jupiter] display-name: JUnit Jupiter ]]></system-out> </testsuite> |
Once we produced JUnit reports with the test results, it is a matter of importing those results into your Jira instance. This can be done by simply submitting automation results to Xray through the REST API, by using one of the available CI/CD plugins (e.g. for Jenkins), or using the Jira interface.
|