- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Guided: Creating a Quotes REST API with Spring Boot 3 - Part 2 - Testing
In this lab, you will gain hands-on experience in adding unit tests to a Spring Boot application, specifically focusing on the Quotes REST API. You will explore various testing approaches, including utilizing the `@SpringBootTest` annotation, extending tests using `TestRestTemplate` and `MockMvc`, and leveraging the `MockBean` annotation to mock service requests. By the end of the lab, you will have a solid foundation for creating your own suite of unit tests, enabling you to confidently make modifications to your applications while ensuring their stability and reliability.
Lab Info
Table of Contents
-
Challenge
Introduction
Welcome to Part 2 of the lab series:
Guided: Creating a Quotes REST API with Spring Boot 3.
This series of labs will guide you through some of the basic concepts for creating a RESTful API Service using Spring Boot with the Spring Web dependency.
In this lab, you will create several different types of tests that begin to help ensure that the Quotes REST API is working correctly.
A benefit of using the Spring Initializr template is the added dependency on
spring-boot-starter-test. This dependency can be seen in thebuild.gradlefile which imports the Spring Boot Test module. This module includes the testing libraries: JUnit, Jupiter, AssertJ, Hamcrest, Mockito, and other useful libraries. Again, thanks to this Spring Boot template, there is little infrastructure that will need to be coded for testing. Simple annotations will help with testing scenarios.The tests in this lab are just an introduction to the possible variations of testing available with Spring Boot Test, and will focus on the Core Spring Boot Test library, including JUnit Jupiter and Mockito.
To get started with this lab, click the Next step > button below.
-
Challenge
Introduction to `@SpringBootTest` and `@Autowired`
Before testing the REST API features, there are few basic configurations of a Spring Boot Test class that need to be covered. As with most things with Spring Boot, annotations will be used to configure a file as a test class.
In the
BasicTestSetup.javafile, you will see an empty class ready to be configured for testing. All of the relevant imports have already been added to the top of the file.The first step to configure this class for testing is to add the
@SpringBootTestannotation above the class declaration:@SpringBootTest public class BasicTestSetup {The
@SpringBootTestannotation is used to configure an application context for testing purposes. It loads the entire application context and all the beans defined in it. This makes it suitable for integration testing, where you want to test the behavior of your application in a real environment.The first test will exercise the
QuotesServiceclass. As with the application code, this service bean can be injected into the test class by decorating the field containing this bean with the@Autowiredannotation:@Autowired private QuotesService quotesService;When you annotate a field or a constructor parameter with
@Autowired, Spring looks for a bean of the same type and injects it automatically. In this case, theQuotesServiceis being injected into the test class, so you can test its behavior.The class is now configured to write the first test against the
QuotesService:@Test public void quotesServiceShouldReturn3Quotes() { var quotes = quotesService.getAllQuotes(); assertEquals(2, quotes.size()); }JUnit Jupiter is a popular testing framework for Java applications. It provides several annotations and assertions that make it easy to write and run tests.
The
@Testannotation marks the method as a test method that should be executed by JUnit.The
assertEquals()assertion checks that the size of the list of all quotes returned from theQuotesServiceis 2. For this example code, theQuotesServiceis hardcoded to return 3 quotes so this assertion will fail.Note:
assertEquals()is one of several assertions available in the Jupiter library. For more advanced assertion methods, thespring-boot-starter-testhas included AssertJ and Hamcrest libraries.- It is also important to recognize that this is a contrived example. In real-world scenarios, the number of records returned by a service is usually not deterministic. However, this issue will be addressed later on in this lab.
To verify that this test is currently failing, execute the following command in the Terminal window:
./gradlew test --tests BasicTestSetupNote: The
--testsflag was added to filter the executed tests to just this current class being worked on.At the command line, you should see that the
BUILD FAILED. If you scroll up in the results, you will see some details as to the failure. Specifically a statement providing the path to the created HTML report. For this lab, you can see this report by opening the link {{localhost:8080}}/build/reports/tests/test/index.html.This initial page of the report will give an overview of all tests that have been run. A link to the specific failed test(s) can be seen under the Failed tests tab. Clicking on that link will display the reason this particular test failed. As expected, the test failed because it was looking for 2 quotes, but actually returned 3.
You can fix this failing test changing the assertion to expect
3instead of2quotes.Rerunning the test command, the build should now pass and you should see
BUILD SUCCESSFUL.This test has started to test the
QuotesServiceclass directly, but it does not verify if the/api/quotesendpoint is functioning correctly. The subsequent steps will address this concern.When ready move to the next step, click the Next step > button below.
-
Challenge
Introduction to `TestRestTemplate`
The prior test class helped to unit test the
QuotesService, but does not test whether the REST API is responding correctly. As seen in Part 1 of this series, you could manually test the REST API by starting the server and executing requests against the embedded server.The same types of tests can be automated in a test class using the
TestRestTemplate. TheTestRestTemplateis a class provided by Spring Boot that makes it easy to test RESTful web services. It provides methods for sending HTTP requests and receiving HTTP responses, similar to a web browser or a REST client.In the
TestRestTemplateTests.javafile, the basics you learned in the previous step have been added. Now that you've seen how to write a basic unit test with Spring Boot it is time to write something a little more interesting that actually tests the REST API.The
TestRestTemplateclient can be injected into this test class using the@Autowiredannotation:@Autowired private TestRestTemplate testRestTemplate;The
TestRestTemplateprovides methods for sending HTTP requests, which means that this test class will start an embedded web server to handle these requests. So that this embedded web server does not conflict with other applications running on this computer, you can configure the embedded server to use a random port. Modify the@SpringBootTestannotation to be:@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class TestRestTemplateTests {Now you can rewrite the previous test to verify that the
/api/quotesendpoint is configured to correctly respond with the same number of quotes:@Test public void quotesEndpointShouldReturn3Quotes () { var results = this.testRestTemplate.getForObject("/api/quotes", List.class); assertEquals(2, results.size()); }The
getForObject()method sends an HTTP GET request to the/api/quotesendpoint and expects the response to be aListobject.The
assertEquals()method performs the same check against the results, still using the wrong number of results. This helps to verify that the test does actually fail when it should.Note: The relative URL
/api/quotescan be used because the test class is using an embedded server with a random port.To verify that this test is currently failing, execute the following command in the Terminal window. Notice the change to pass in this test class:
./gradlew test --tests TestRestTemplateTestsYou can see the test results report at {{localhost:8080}}/build/reports/tests/test/index.html.
Note: If you still had the report open from the prior step, but drilled into the failing test, that specific static test class page no longer exists since the above command ran for this particular test class.
Again, you can fix this failing test by changing the assertion to expect
3instead of2quotes. Then rerunning the test command, the build should now pass withBUILD SUCCESSFUL.When ready move to the next step, click the Next step > button below.
-
Challenge
Introduction to `MockMVC`
In the previous step, we used the
TestRestTemplateclass to send HTTP requests and receive HTTP responses. While this is a useful approach for testing the entire stack, depending on the embedded server, it can be slow and cumbersome to set up.In this step, you'll use the
MockMvcclass. This approach allows us to test our controller logic in isolation, without starting an embedded server or sending real HTTP requests. It provides methods for sending simulated HTTP requests and receiving simulated HTTP responses, similar to a mock HTTP client. This class also provides additional features, like support for testing JSON and XML payloads, among others.In the
MockMvcTests.javafile, the required import statements have been added, andMockMvccan be configured. One way to configure the test class to useMockMvcis to use Spring Boot annotations. Such as the following changes to the test class:@SpringBootTest @AutoConfigureMockMvc public class MockMvcTests { @Autowired private MockMvc mockMvc;@AutoConfigureMockMvctells Spring Boot to automatically configure a MockMvc instance and inject it into the test class using themockMvcfield with the@Autowiredannotation.Note: If you need more control over the configuration of the
MockMvcinstance, you could use theMockMvcBuildersutility class.Now you can refactor the
quotesEndpointShouldReturn3Quotestest from the previous step to use themockMvcinstance:@Test public void quotesEndpointShouldReturn3Quotes() throws Exception { this.mockMvc.perform(get("/api/quotes")) .andExpect(status().isOk()) .andExpect(jsonPath("$.length()").value(3)); }Observe that the method signature includes
throws Exceptionfor error handling purposes. While theTestRestTemplatehandles potential exceptions in a suppressed manner,MockMvcmanages them directly in a different approach to test such scenarios.The
mockMvc.perform()method accepts aRequestBuilderobject. This object is being built with theMockMvcRequestBuilders.get()method to send a simulated HTTP GET request to the/api/quotesendpoint.Assertions can then be made against the simulated response from this request using various
ResultMatchers. These assertions can be chained together using either theandExpect()orandExpectAll()Fluent API methods.In this case, the response is first checked that the returned status code is
200 OK. Then the expected JSON response is checked that itslengthproperty is equal to the expected 3 quotes.By chaining these methods together, we're able to create a single, expressive assertion that checks the length of the JSON array returned by the API. This makes the test more readable and easier to understand than if we were to use multiple lines of code to create the same assertion.
This test can be verified by running the command:
./gradlew test --tests MockMvcTestsYou can see the test results report at {{localhost:8080}}/build/reports/tests/test/index.html.
Compared to the
TestRestTemplate, this test should be quicker since it doesn't rely on an embedded server. This is particularly valid when more tests are added.This contrived test is still dependent on the
QuoteServicereturning an exact number of quotes. This will be addressed in the next step.When ready move to the next step, click the Next step > button below.
-
Challenge
Introduction to `MockBean`
The previous tests have utilized the
QuotesService's actual instance, which has functioned well so far as the service returns precisely three quotes in this contrived example. However, this assumption cannot be made for an actual production service, as the service may include other dependencies like a persistence layer. This can result in fragile tests that rely on assumptions and extra layers that should not be tested at this time.To help alleviate these issues, the
QuotesServicecan be mocked to return an expected result and remove the dependency on any other layers that the service may also depend on.In the same
MockMvcTests.javafile, you can add the following code just above the test method :@MockBean private QuotesService quotesService;When the
@MockBeanannotation is added to theQuotesServicefield, a mocked version of the service will be injected. By mocking theQuotesService, the test for the/api/quotesendpoint can be separated from the actual implementation's dependencies, and you can regulate the data returned by theQuotesServicewhile testing.The
Mockito.when()method can now be used to declare what should be returned when thegetAllQuotes()method is called.At the top of the
quotesEndpointShouldReturn3Quotes()test method, add the following lines to mock what is returned by thegetAllQuotes()method:@Test public void quotesEndpointShouldReturn3Quotes() throws Exception { List<String> quotes = Arrays.asList("quote1", "quote2"); Mockito.when(quotesService.getAllQuotes()).thenReturn(quotes);In this case, the mock will return a list of 2 quotes. You can now be certain of any assertions made about the behavior of the
/api/quotesendpoint.Rerunning the gradlew test command for this class file:
./gradlew test --tests MockMvcTestsUnless you changed the expected results to
2, you will notice that the build again failed. Reviewing the test results report at {{localhost:8080}}/build/reports/tests/test/index.html, you will see that it is now returning 2 quotes instead of 3.When ready to move to the next step, click the Next step > button below.
-
Challenge
Next Steps
Congratulations on completing Part 2 of this guided lab series on Spring!
You are welcome to continue working with this lab to see what improvements you can make.
Some ideas are to:
- Create a
MockMvctest for the/api/quotes/0endpoint that mocks thegetQuoteByIndex(int index)method. - Create a
MockMvctest for the POST/api/quotesmethod. Note that this method returns a201 Createdstatus. - Look up the
HamcrestorAssertJassertion libraries to see some of the advanced assertions that could be used.
The best to you as you continue your learning journey here at Pluralsight. Hope to see you in Part 3 of this series.
- Create a
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.