|
|
Spring Framework is a well-known Java framework to build Java based applications, supporting IoC (Inversion of Control) principle.
Sprint Boot provides an oppiniated 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:
@WebMvcTest
@SpringBootTest
We'll implement JUnit 5 tests for all these layers:
Spring Boot supports test slicing; the idea is to provides slices of the whole ApplicationContext by loading lesser components and thus provide 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 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.
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.
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 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")); } @XrayTest(key = "XRAY-1") @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"); } @XrayTest(key = "XRAY-2") @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 goal (i.e., the integration tests ran by failsafe on the previous mvn command).
In this example, all tests have succeed, 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-12T17:42:55"> <properties> <property name="CONSOLE_LOG_CHARSET" value="UTF-8"/> <property name="FILE_LOG_CHARSET" value="UTF-8"/> <property name="PID" value="45881"/> <property name="apple.awt.application.name" value="ForkedBooter"/> <property name="basedir" value="/Users/sergio/exps/tutorial-spring"/> <property name="catalina.base" value="/private/var/folders/2w/5zhst_816kl0dcfv4t7g3vv00000gn/T/tomcat.0.9109224442063392217"/> <property name="catalina.home" value="/private/var/folders/2w/5zhst_816kl0dcfv4t7g3vv00000gn/T/tomcat.0.16552584680725469127"/> <property name="catalina.useNaming" value="false"/> <property name="com.zaxxer.hikari.pool_number" value="1"/> <property name="file.encoding" value="UTF-8"/> <property name="file.separator" value="/"/> <property name="ftp.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/> <property name="http.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/> <property name="java.awt.headless" value="true"/> <property name="java.class.version" value="65.0"/> <property name="java.home" value="/usr/local/Cellar/openjdk/21.0.1/libexec/openjdk.jdk/Contents/Home"/> <property name="java.io.tmpdir" value="/var/folders/2w/5zhst_816kl0dcfv4t7g3vv00000gn/T/"/> <property name="java.runtime.name" value="OpenJDK Runtime Environment"/> <property name="java.runtime.version" value="21.0.1"/> <property name="java.specification.name" value="Java Platform API Specification"/> <property name="java.specification.vendor" value="Oracle Corporation"/> <property name="java.specification.version" value="21"/> <property name="java.vendor" value="Homebrew"/> <property name="java.vendor.url" value="https://github.com/Homebrew/homebrew-core/issues"/> <property name="java.vendor.url.bug" value="https://github.com/Homebrew/homebrew-core/issues"/> <property name="java.vendor.version" value="Homebrew"/> <property name="java.version" value="21.0.1"/> <property name="java.version.date" value="2023-10-17"/> <property name="java.vm.compressedOopsMode" value="Zero based"/> <property name="java.vm.info" value="mixed mode, sharing"/> <property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/> <property name="java.vm.specification.name" value="Java Virtual Machine Specification"/> <property name="java.vm.specification.vendor" value="Oracle Corporation"/> <property name="java.vm.specification.version" value="21"/> <property name="java.vm.vendor" value="Homebrew"/> <property name="java.vm.version" value="21.0.1"/> <property name="jdk.debug" value="release"/> <property name="line.separator" value=" "/> <property name="localRepository" value="/Users/sergio/.m2/repository"/> <property name="native.encoding" value="UTF-8"/> <property name="org.jboss.logging.provider" value="slf4j"/> <property name="os.arch" value="x86_64"/> <property name="os.name" value="Mac OS X"/> <property name="os.version" value="14.2.1"/> <property name="path.separator" value=":"/> <property name="socksNonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/> <property name="stderr.encoding" value="UTF-8"/> <property name="stdout.encoding" value="UTF-8"/> <property name="sun.arch.data.model" value="64"/> <property name="sun.boot.library.path" value="/usr/local/Cellar/openjdk/21.0.1/libexec/openjdk.jdk/Contents/Home/lib"/> <property name="sun.cpu.endian" value="little"/> <property name="sun.io.unicode.encoding" value="UnicodeBig"/> <property name="sun.java.launcher" value="SUN_STANDARD"/> <property name="sun.jnu.encoding" value="UTF-8"/> <property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/> <property name="user.country" value="PT"/> <property name="user.dir" value="/Users/sergio/exps/tutorial-spring"/> <property name="user.home" value="/Users/sergio"/> <property name="user.language" value="en"/> <property name="user.name" value="sergio"/> <property name="user.timezone" value="Europe/Lisbon"/> </properties> <testcase name="getPersonalizedGreeting" classname="com.idera.xray.tutorials.springboot.GreetingControllerMockedIT" time="0" started-at="2024-02-12T17:42:53.713267" finished-at="2024-02-12T17:42:54.038343"> <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-12T17:42:54.540558" finished-at="2024-02-12T17:42:54.82271"> <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="test_key" value="XRAY-2"/> <property name="_dummy_" value=""/> </properties> </testcase> <testcase name="getUserUnsuccess" classname="com.idera.xray.tutorials.springboot.UserRestControllerIT" time="0" started-at="2024-02-12T17:42:54.896158" finished-at="2024-02-12T17:42:54.917811"> <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-12T17:42:54.856928" finished-at="2024-02-12T17:42:54.876068"> <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-12T17:42:54.876839" finished-at="2024-02-12T17:42:54.895425"> <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="test_key" value="XRAY-1"/> <property name="_dummy_" value=""/> </properties> </testcase> <testcase name="getDefaultGreeting" classname="com.idera.xray.tutorials.springboot.GreetingControllerMockedIT" time="0" started-at="2024-02-12T17:42:54.039385" finished-at="2024-02-12T17:42:54.044234"> <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-12T17:42:55.133942" finished-at="2024-02-12T17:42:55.143765"> <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="_dummy_" value=""/> </properties> </testcase> <testcase name="getUserWithSuccess" classname="com.idera.xray.tutorials.springboot.UserRestControllerIT" time="0" started-at="2024-02-12T17:42:54.823735" finished-at="2024-02-12T17:42:54.856158"> <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-12T17:42:54.936475" finished-at="2024-02-12T17:42:54.958408"> <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-12T17:42:52.693447" finished-at="2024-02-12T17:42:53.384655"> <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-12T17:42:54.918649" finished-at="2024-02-12T17:42:54.935546"> <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> |
Notes:
npx cypress open
cypress.config.js
to define configuration values such as taking screenshots, recordings or the reporter to use (more info here).As we saw in the previous example, where we are producing JUnit reports with the test results. It is now a matter of importing those results to 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 to do so.
|
.toc-btf { position: fixed; } |