Learn how to use the tools provided in this tutorial and write unit tests and integration tests in a Spring Boot environment.
1. Overview
In this article, we will learn how to write unit tests and integrate them intoSpring BootIn the environment. You can find a lot of tutorials on this topic online, but it is difficult to find all the information you need in one page. I often notice that junior developers confuse the concepts of unit testing and integration testing, especially when it comes to the Spring ecosystem. I will try to clarify how different annotations are used in different contexts.
2. Unit Test vs. Integration Test
That's what Wikipedia saysUnit Testingof:
In computer programming, unit testing is a software testing method used to test a single unit of source code, a collection of one or more computer program modules, and related control data, usage processes and operational processes to determine whether they are suitable for use.
Integration Testing:
“Integration testing (sometimes called integration and testing, abbreviated as I&T) is a phase of software testing in which individual software modules are combined to perform testing.”
In short, when we are doing unit testing, we just test a unit of code, testing only one method at a time, not all other components that interact with the component that is testing.
On the other hand, in integration testing, we test the integration between the components. Due to unit testing, we can tell that these components behave consistently with the needs, but it is not clear how they work together. This is the responsibility of integration testing.
3. Java unit test
All Java developers know that JUnit is the main framework for performing unit tests. It provides many annotations to assert expectations.
Hamcrest is an additional framework for software testing. Hamcrest allows the use of existing matcher classes to check conditions in the code, and also allows custom matcher implementations. To use Hamcrest matcher in JUnit, you must useassertThat
Statement followed by one or more matchers.
Here you can see simple tests using both frameworks:
import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import ; import ; import ; public class AssertTests { @Test public void testAssertArrayEquals() { byte[] expected = "trial".getBytes(); byte[] actual = "trial".getBytes(); assertArrayEquals("failure - byte arrays not same", expected, actual); } @Test public void testAssertEquals() { assertEquals("failure - strings are not equal", "text", "text"); } @Test public void testAssertFalse() { assertFalse("failure - should be false", false); } @Test public void testAssertNotNull() { assertNotNull("should not be null", new Object()); } @Test public void testAssertNotSame() { assertNotSame("should not be same Object", new Object(), new Object()); } @Test public void testAssertNull() { assertNull("should be null", null); } @Test public void testAssertSame() { Integer aNumber = (768); assertSame("should be same", aNumber, aNumber); } // JUnit Matchers assertThat @Test public void testAssertThatBothContainsString() { assertThat("albumen", both(containsString("a")).and(containsString("b"))); } @Test public void testAssertThatHasItems() { assertThat(("one", "two", "three"), hasItems("one", "three")); } @Test public void testAssertThatEveryItemContainsString() { assertThat((new String[] { "fun", "ban", "net" }), everyItem(containsString("n"))); } // Core Hamcrest Matchers with assertThat @Test public void testAssertThatHamcrestCoreMatchers() { assertThat("good", allOf(equalTo("good"), startsWith("good"))); assertThat("good", not(allOf(equalTo("bad"), equalTo("good")))); assertThat("good", anyOf(equalTo("bad"), equalTo("good"))); assertThat(7, not(CombinableMatcher.<Integer> either(equalTo(3)).or(equalTo(4)))); assertThat(new Object(), not(sameInstance(new Object()))); } @Test public void testAssertTrue() { assertTrue("failure - should be true", true); } }
4. Introduction to our case
Let's write a simple program. The purpose is to provide a basic search engine for comics.
4.1. Maven dependencies
First, we need to add some dependencies to our project.
<dependency> <groupId></groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId></groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId></groupId> <artifactId>lombok</artifactId> <version>1.16.20</version> <scope>provided</scope> </dependency>
4.2. Definition Model
Our model is very simple, consisting of only two classes: Manga and MangaResult
4.2.1. Manga Class
The Manga class represents the Manga instance retrieved by the system. Use Lombok to reduce boilerplate code.
package ; import ; import ; import ; import ; import ; @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class Manga { private String title; private String description; private Integer volumes; private Double score; }
4.2.2. MangaResult
The MangaResult class is a wrapper class that contains a Manga List.
package ; import ; import ; import ; import ; @Getter @Setter @NoArgsConstructor public class MangaResult { private List<Manga> result; }
4.3. Implement Service
To implement this Service, we will use the free API interface provided by Jikan Moe.
RestTemplate is a Spring class used to initiate REST calls to the API.
package ; import ; import org.; import org.; import ; import ; import ; import ; import ; @Service public class MangaService { Logger logger = (); private static final String MANGA_SEARCH_URL="/search/manga/"; @Autowired RestTemplate restTemplate; public List<Manga> getMangasByTitle(String title) { return (MANGA_SEARCH_URL+title, ).getBody().getResult(); } }
4.4. Implement Controller
The next step is to write a REST Controller that exposes two endpoints, one synchronous and the other asynchronous, which is only used for testing purposes. The Controller uses the Service defined above.
package ; import ; import ; import org.; import org.; import ; import ; import ; import ; import ; import ; import ; import ; import ; @RestController @RequestMapping(value = "/manga") public class MangaController { Logger logger = (); @Autowired private MangaService mangaService; @RequestMapping(value = "/async/{title}", method = ) @Async public CompletableFuture<List<Manga>> searchASync(@PathVariable(name = "title") String title) { return ((title)); } @RequestMapping(value = "/sync/{title}", method = ) public @ResponseBody <List<Manga>> searchSync(@PathVariable(name = "title") String title) { return (title); } }
4.5. Start and test the system
mvn spring-boot:run
Then, Let's try it:
curl http://localhost:8080/manga/async/ken curl http://localhost:8080/manga/sync/ken
Sample output:
{ "title":"Rurouni Kenshin: Meiji Kenkaku Romantan", "description":"Ten years have passed since the end of Bakumatsu, an era of war that saw the uprising of citizens against the Tokugawa shogunate. The revolutionaries wanted to create a time of peace, and a thriving c...", "volumes":28, "score":8.69 }, { "title":"Sun-Ken Rock", "description":"The story revolves around Ken, a man from an upper-class family that was orphaned young due to his family's involvement with the Yakuza; he became a high school delinquent known for fighting. The only...", "volumes":25, "score":8.12 }, { "title":"Yumekui Kenbun", "description":"For those who suffer nightmares, help awaits at the Ginseikan Tea House, where patrons can order much more than just Darjeeling. Hiruko is a special kind of a private investigator. He's a dream eater....", "volumes":9, "score":7.97 }
5. Unit testing of Spring Boot applications
Spring Boot provides a powerful class to make testing simple:@SpringBootTestannotation
This annotation can be specified on a test class running based on Spring Boot.
In addition to the regular Spring TestContext Framework, it also provides the following features:
- When @ContextConfiguration (loader=…) is not specifically declared, SpringBootContextLoader is used as the default ContextLoader.
- Automatically search for @SpringBootConfiguration when the nested @Configuration annotation is not used and the relevant class is not explicitly specified.
- Allows customization of Environment properties using Properties.
- Provides support for different web environment modes, including the ability to start a fully running web server on defined or random ports.
- Register the TestRestTemplate and/or WebTestClient Bean for use in web tests that run completely on the web server.
Here we only have two components to test: MangaService and MangaController
5.1. Perform unit testing of MangaService
To test MangaService, we need to isolate it from external components. In this example, only one external component is needed: RestTemplate, which we use to call the remote API.
What we need to do is simulate the RestTemplate Bean and have it always respond with a fixed given response. Spring Test combines and extends the Mockito library. Through the @MockBean annotation, we can configure mock beans.
package ; import static ; import static ; import ; import ; import ; import ; import ; import ; import ; import ; import ; import .; import ; import static ; import ; import ; import ; import ; @RunWith() @SpringBootTest public class MangaServiceUnitTest { @Autowired private MangaService mangaService; // MockBean is the annotation provided by Spring that wraps mockito one // Annotation that can be used to add mocks to a Spring ApplicationContext. // If any existing single bean of the same type defined in the context will be replaced by the mock, if no existing bean is defined a new one will be added. @MockBean private RestTemplate template; @Test public void testGetMangasByTitle() throws IOException { // Parsing mock file MangaResult mRs = JsonUtils.jsonFile2Object("", ); // Mocking remote service when((any(), any())).thenReturn(new ResponseEntity(mRs, )); // I search for goku but system will use mocked response containing only ken, so I can check that mock is used. List<Manga> mangasByTitle = ("goku"); assertThat(mangasByTitle).isNotNull() .isNotEmpty() .allMatch(p -> () .toLowerCase() .contains("ken")); } }
5.2. Unit test of MangaController
As done in unit tests for MangaService, we need to isolate components. In this case, we need to simulate the MangaService Bean.
Then we have another problem... The Controller part is part of the system that manages HttpRequest, so we need a system to simulate this behavior rather than starting a full HTTP server.
MockMvc is the Spring class that performs this operation. It can be set up in different ways:
- Using Standalone Context
- Using WebApplication Context
- Let Spring load all contexts by using @SpringBootTest and @AutoConfigureMockMvc on the test class to achieve automatic assembly
- Let Spring load the web layer context by using the @WebMvcTest annotation on the test class for automatic assembly
package ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import ; import ; import ; import ; import ; import ; import ; import ; import ; import .; import ; import ; import ; import ; import ; import ; @SpringBootTest @RunWith() public class MangaControllerUnitTest { MockMvc mockMvc; @Autowired protected WebApplicationContext wac; @Autowired MangaController mangaController; @MockBean MangaService mangaService; /** * List of samples mangas */ private List<Manga> mangas; @Before public void setup() throws Exception { = standaloneSetup().build();// Standalone context // mockMvc = (wac) // .build(); Manga manga1 = () .title("Hokuto no ken") .description("The year is 199X. The Earth has been devastated by nuclear war...") .build(); Manga manga2 = () .title("Yumekui Kenbun") .description("For those who suffer nightmares, help awaits at the Ginseikan Tea House, where patrons can order much more than just Darjeeling. Hiruko is a special kind of a private investigator. He's a dream eater....") .build(); mangas = new ArrayList<>(); (manga1); (manga2); } @Test public void testSearchSync() throws Exception { // Mocking service when((any())).thenReturn(mangas); (get("/manga/sync/ken").contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].title", is("Hokuto no ken"))) .andExpect(jsonPath("$[1].title", is("Yumekui Kenbun"))); } @Test public void testSearchASync() throws Exception { // Mocking service when((any())).thenReturn(mangas); MvcResult result = (get("/manga/async/ken").contentType(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(request().asyncStarted()) .andDo(print()) // .andExpect(status().is2xxSuccessful()).andReturn(); .andReturn(); // ().getAsyncContext().setTimeout(10000); (asyncDispatch(result)) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].title", is("Hokuto no ken"))); } }
As you can see in the code, the first solution is chosen because it is the lightest one, and we can have better governance of objects loaded in the Spring context.
In asynchronous testing, you must first simulate the asynchronous behavior by calling the service and then starting the asyncDispatch method.
6. Integration testing of Spring Boot applications
For integration testing, we want to provide downstream communication to check our main components.
6.1. Integration test of MangaService
This test is also very simple. We don't need to mock anything, because our purpose is to call the remote Manga API.
package ; import static ; import ; import ; import ; import ; import ; import .; import ; import ; @RunWith() @SpringBootTest public class MangaServiceIntegrationTest { @Autowired private MangaService mangaService; @Test public void testGetMangasByTitle() { List<Manga> mangasByTitle = ("ken"); assertThat(mangasByTitle).isNotNull().isNotEmpty(); } }
6.2. Integration test of MangaController
This test is very similar to unit tests, but in this case we no longer need to mock MangaService.
package ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import static ; import ; import ; import ; import ; import ; import ; import .; import ; import ; import ; import ; @SpringBootTest @RunWith() public class MangaControllerIntegrationTest { // @Autowired MockMvc mockMvc; @Autowired protected WebApplicationContext wac; @Autowired MangaController mangaController; @Before public void setup() throws Exception { = standaloneSetup().build();// Standalone context // mockMvc = (wac) // .build(); } @Test public void testSearchSync() throws Exception { (get("/manga/sync/ken").contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.*.title", hasItem(is("Hokuto no Ken")))); } @Test public void testSearchASync() throws Exception { MvcResult result = (get("/manga/async/ken").contentType(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(request().asyncStarted()) .andDo(print()) .andReturn(); (asyncDispatch(result)) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.*.title", hasItem(is("Hokuto no Ken")))); } }
7. Conclusion
We have learned about the main differences between unit testing and integration testing in Spring Boot environments, and we have learned about frameworks that simplify testing writing like Hamcrest. Of course, it can also be in mineGitHub repositoryAll codes are found in.
original:/articles/unit-and-integration-tests-in-spring-boot-2
Author: Marco Giglione
The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.