Introduction :#
Integration testing is a crucial phase in software development where individual modules or components of a system are tested together to ensure they work seamlessly as a whole. Unlike unit testing, which focuses on testing small units of code in isolation, integration testing aims to validate the interactions and interfaces between different components. This type of testing helps identify issues that might not be apparent during unit testing, such as problems with data flow, communication, or integration with external systems like databases or APIs. By simulating real-world scenarios, integration testing ensures that the components of a system function correctly when integrated, ultimately leading to a more reliable and robust software product.
Real life analogy#
Imagine you’re organizing a large dinner party where different people are responsible for different dishes: one person is making the main course, another is handling the appetizers, and yet another is preparing the dessert. Before the party, each dish might be tested separately to ensure it tastes good on its own.
Integration testing in this scenario is like having a taste test of the entire meal to ensure that all the dishes work well together when served as a complete dinner. You check if the flavors complement each other, if the timing of serving each dish is right, and if everything is prepared correctly. If there’s a problem—like the dessert is too sweet and clashes with the main course—you’ll catch it during this combined taste test, ensuring that when the guests arrive, everything comes together perfectly.
Importance of Integration Testing#
Integration testing indeed plays a vital role in identifying issues that arise from the interactions between different components or services. By focusing on how these parts work together, integration tests help catch issues like:
- Configuration Errors: Problems with how components are set up to interact with each other.
- Missing Data: Situations where required data is not properly passed between components.
- Incorrect Business Logic: Issues that arise when different components handle data or operations in unexpected ways.
Moreover, integration tests are crucial for maintaining the stability of the system when changes are made. They help ensure that new updates or modifications don’t inadvertently break existing functionality, thus making regression testing more effective and safeguarding the overall integrity of the application.
Example
Think of integration testing like rehearsing a play with the entire cast before the actual performance. Each actor (module or component) has been trained individually and knows their lines and actions (unit testing).
During rehearsal (integration testing), you bring everyone together to practice the full play (the entire system). This allows you to see how the actors interact on stage, if the scenes transition smoothly, and if the timing of cues is right.
If an actor forgets their line or if the scene doesn’t flow well, it only becomes apparent when the full cast is involved. Similarly, integration testing reveals issues that might not be visible when each part of the system is tested in isolation, ensuring that everything works harmoniously when the "show" goes live.
Testing The Presentation Layer#
Testing the Presentation Layer in a Spring Boot application focuses on verifying the functionality of the controller or web layer (also known as the presentation layer). This layer handles incoming HTTP requests, interacts with the service layer, and sends appropriate responses back to the client. Testing the presentation layer ensures that the web endpoints (REST API or MVC controllers) are working as intended.
Key Characteristics of This Test:#
- Full Application Context:
- The test uses
@SpringBootTest
, which loads the full Spring application context, making it a true integration test across multiple layers (controller, service, and repository).
- The test uses
- Web Layer Testing:
- It uses
@AutoConfigureWebTestClient
with aWebTestClient
, which allows testing REST endpoints in a non-blocking manner, making HTTP requests to the web layer and verifying the HTTP responses. timeout = "100000"
: Sets a custom timeout for requests made byWebTestClient
. This is useful for ensuring that tests do not fail due to slow responses.TestContainerConfiguration.class
: This would be a custom configuration class that you’ve defined to set up things like test containers or other test-specific beans.
- It uses
- Database Integration:
- The test interacts with a real or in-memory database using the
EmployeeRepository
. The database is reset (employeeRepository.deleteAll()
) in the@BeforeEach
method to ensure that each test runs in isolation with a clean state.
- The test interacts with a real or in-memory database using the
- Different Scenarios Covered:
- The test covers various scenarios such as:
- Successful retrieval of an employee by ID.
- Handling cases where an employee does not exist (404 Not Found).
- Creating, updating, and deleting employees, including validations for invalid operations (e.g., updating email when it's restricted).
- The test covers various scenarios such as:
Using WebTestClient#
WebTestClient provides a fluent API and supports making real HTTP requests to your application. Although WebTestClient is primarily associated with WebFlux applications, it can also be used with Spring MVC applications when set up correctly. Use the @AutoConfigureWebTestClient to auto configure the WebTestClient.
WebTestClient Testing Methods#
Here’s a brief explanation of the common WebTestClient
response methods and how they’re used in testing:
exchange()
: Executes the request and returns aWebTestClient.ResponseSpec
. This method is often used to initiate the request and obtain the response for further assertions or processing.expectStatus()
: Asserts the status code of the response. For example, you can check if the response status is200 OK
or404 Not Found
, ensuring that the request was processed correctly.expectBody()
: Asserts the body of the response. This method allows you to verify the content of the response body, such as checking if it contains expected values or matches a specific format.expectHeader()
: Asserts the headers of the response. You can use this method to check if the response includes the correct headers and their values, ensuring that necessary metadata is correctly returned..jsonPath("$.id").isNotEmpty()
: This method checks if a specific JSON path (e.g.,$.id
) in the response body is not empty. It ensures that the JSON object contains a value for the specified field..jsonPath("$.name").isEqualTo("Jane Doe")
: This method asserts that the value of the JSON path$.name
in the response body matches the expected value, in this case, "Jane Doe"..jsonPath("$.email").isEqualTo("jane.doe@example.com")
: Similar to the previous example, this method verifies that the value of the JSON path$.email
in the response body is equal to "jane.doe@example.com".
These methods are used in combination to validate different aspects of the response and ensure that the web application behaves as expected under test conditions.
Test Cases for Controller Layer#
- Controller Class
Assume the following EmployeeController
class that handles basic CRUD operations:
Press CTRL + SHIFT + T on EmployeeController . And create a test class.
- Service Class
Here’s the business logics.
- Custom Exception Class
To handle all exceptions.
- Unit Test Cases for
EmployeeController
The primary class being tested is the EmployeeController
, which handles HTTP requests (GET, POST, PUT, DELETE) and delegates the business logic to the service layer.
- Explanations
This EmployeeControllerIntegrationTest
class is a well-structured integration test suite for an employee management REST API using Spring Boot. The test suite covers various operations related to employees, such as creating, reading, updating, and deleting employee records. The primary class being tested is the EmployeeController
, which handles HTTP requests (GET, POST, PUT, DELETE) and delegates the business logic to the service layer.
Here's a breakdown of the main tests implemented:
- Test Setup (
@BeforeEach
)- Each test starts by creating a sample
EmployeeEntity
andEmployeeDto
instance. - Before each test, the
employeeRepository.deleteAll()
is called to ensure a clean database state, avoiding interference from previous tests.
- Each test starts by creating a sample
- Test Cases
- Test for Successful Get Request
- Get Employee by ID (Success):
- Saves an employee to the database and verifies the successful retrieval via
GET /employees/{id}
. - Asserts the status code
200 OK
and validates the returned employee data usingjsonPath()
.
- Saves an employee to the database and verifies the successful retrieval via
- Get Employee by ID (Failure):
- Tries to retrieve an employee with a non-existing ID and asserts that the status is
404 Not Found
.
- Tries to retrieve an employee with a non-existing ID and asserts that the status is
- Get Employee by ID (Success):
- Test for Successful Get Request
- Test for Successful Post Request
- Create New Employee (When Already Exists):
- Saves an employee first and then tries to create the same employee again. This triggers an error and checks for a
500 Server Error
.
- Saves an employee first and then tries to create the same employee again. This triggers an error and checks for a
- Create New Employee (Success):
- Attempts to create a new employee with valid data and asserts the employee was successfully created (
201 Created
) with matching data.
- Attempts to create a new employee with valid data and asserts the employee was successfully created (
- Create New Employee (When Already Exists):
- Test for Successful PUT Request
- Update Employee (When Does Not Exist):
- Tries to update a non-existing employee and verifies that the status returned is
404 Not Found
.
- Tries to update a non-existing employee and verifies that the status returned is
- Update Employee (When Email Changed):
- Ensures that attempting to update an employee’s email results in a
500 Server Error
.
- Ensures that attempting to update an employee’s email results in a
- Update Employee (When Valid):
- Updates the employee’s name and salary and verifies that the updates are applied successfully (
200 OK
).
- Updates the employee’s name and salary and verifies that the updates are applied successfully (
- Update Employee (When Does Not Exist):
- Test for Successful Delete Request
- Delete Employee (When Does Not Exist):
- Tries to delete an employee that doesn’t exist and asserts that the response status is
404 Not Found
.
- Tries to delete an employee that doesn’t exist and asserts that the response status is
- Delete Employee (Success):
- Deletes an existing employee and checks that it’s removed correctly (
204 No Content
), followed by verifying that the same employee cannot be deleted again (404 Not Found
).
- Deletes an existing employee and checks that it’s removed correctly (
- Delete Employee (When Does Not Exist):
- Outputs
- Output of Successful Get Request
Output of Get Employee by ID (Success):
- Output of Successful Get Request
Output of Get Employee by ID (Failure):
- Output of Successful Post Request
Output of Create New Employee (When Already Exists)
Output of Create New Employee (Success):
- Output of Successful PUT Request
Output of Update Employee (When Does Not Exist):
Output of Update Employee (When Email Changed):
Output of Update Employee (When Valid):
- Output of Successful Delete Request
Delete Employee (When Does Not Exist):
Delete Employee (Success):
Here, we have already covered these aspects, which are highlighted in green in the image.