Appearance
Testing
This document outlines the testing strategies, frameworks, and procedures for the IT Ticketing Service. A robust testing suite is crucial for maintaining code quality, ensuring reliability, and preventing regressions. All developers are expected to write and maintain tests for any new features or bug fixes.
Testing Framework
The project leverages the spring-boot-starter-test dependency, which provides a comprehensive, out-of-the-box testing framework. This starter transitively includes the primary libraries used for testing across the application.
The core testing stack consists of:
- Spring Test & Spring Boot Test: Provides utilities for loading Spring application contexts in tests, dependency injection of beans, and tools for integration testing (e.g.,
MockMvc,@SpringBootTest). - JUnit 5 (Jupiter): The next generation of JUnit, used as the primary framework for writing and running test cases.
- Mockito: A powerful mocking framework used to create mock objects of dependencies, allowing for true unit testing by isolating the component under test.
- AssertJ: Provides a fluent API for writing assertions, making test verification more readable and expressive.
- Hamcrest: A library of matcher objects for writing flexible and readable assertions.
The following dependency from the pom.xml includes all the necessary libraries:
xml
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>1
2
3
4
5
6
2
3
4
5
6
Unit Testing
Unit tests are designed to verify the logic of a single component (e.g., a service class, a utility) in isolation. Dependencies of the component under test are mocked to ensure that the test focuses solely on the component's own behavior.
Testing the Service Layer
The primary focus of unit testing in this application is the service layer, where most of the business logic resides. When testing a service, its dependencies, such as the TicketRepository or RabbitTemplate, must be mocked.
Best Practices & Implementation:
- Isolate the Class: Use Mockito's
@Mockand@InjectMocksannotations to create mock dependencies and inject them into the service instance being tested. - Stub Method Calls: Use
Mockito.when(...).thenReturn(...)to define the behavior of mocked dependencies. For example, when testing afindByIdservice method, you would stub therepository.findById()call to return a specific ticket object. - Verify Behavior: Use
Mockito.verify(...)to ensure that methods on your mocks were called with the expected arguments. For instance, verify thatrepository.save()was called exactly once. - Assert the Outcome: Use AssertJ assertions (
assertThat(...)) to check the return value of the service method or the state of the object after the method execution.
Example: Unit Test for TicketService
java
// Assumed structure for a TicketService unit test
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
// Use MockitoExtension to enable annotations
@ExtendWith(MockitoExtension.class)
class TicketServiceTest {
// Create a mock of the repository dependency
@Mock
private TicketRepository ticketRepository;
// Create a mock for the RabbitMQ template
@Mock
private RabbitTemplate rabbitTemplate;
// Create an instance of the service and inject the mocks into it
@InjectMocks
private TicketService ticketService;
@Test
void givenValidTicketId_whenFindById_thenReturnsTicket() {
// Arrange: Define the behavior of the mock
Ticket mockTicket = new Ticket();
mockTicket.setId(1L);
mockTicket.setTitle("Test Ticket");
when(ticketRepository.findById(1L)).thenReturn(Optional.of(mockTicket));
// Act: Call the method under test
Optional<Ticket> foundTicket = ticketService.findTicketById(1L);
// Assert: Verify the outcome
assertThat(foundTicket).isPresent();
assertThat(foundTicket.get().getId()).isEqualTo(1L);
assertThat(foundTicket.get().getTitle()).isEqualTo("Test Ticket");
// Verify that the repository method was called
verify(ticketRepository).findById(1L);
}
@Test
void givenNewTicket_whenCreateTicket_thenSavesAndPublishesEvent() {
// Arrange
Ticket newTicket = new Ticket();
newTicket.setTitle("New Laptop Request");
// When save is called, return the object with an ID to simulate persistence
when(ticketRepository.save(any(Ticket.class))).thenAnswer(invocation -> {
Ticket t = invocation.getArgument(0);
t.setId(100L);
return t;
});
// Act
Ticket createdTicket = ticketService.createTicket(newTicket);
// Assert
assertThat(createdTicket.getId()).isNotNull();
assertThat(createdTicket.getTitle()).isEqualTo("New Laptop Request");
// Verify that the repository's save method was called once
verify(ticketRepository, times(1)).save(newTicket);
// Verify that an event was published to RabbitMQ
verify(rabbitTemplate, times(1)).convertAndSend(eq("ticket.exchange"), eq("ticket.routing.key"), any(TicketEvent.class));
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
Integration Testing
Integration tests verify that different components of the application work together as expected. This includes testing the web layer (controllers), data persistence layer, and messaging integration.
Testing Controllers with MockMvc
Controller tests validate the API endpoints, including request/response formats, HTTP status codes, and request validation. MockMvc allows us to simulate HTTP requests and assert the responses without needing to run a full web server.
@SpringBootTest: Loads the complete Spring ApplicationContext.@AutoConfigureMockMvc: Configures aMockMvcinstance for use in the test.@MockBean: Replaces a bean in the application context with a Mockito mock. This is useful for mocking the service layer to prevent tests from hitting the actual database.
Example: Controller Integration Test
java
// Assumed structure for a TicketController integration test
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
class TicketControllerTest {
@Autowired
private MockMvc mockMvc;
// Mock the service layer to isolate the controller
@MockBean
private TicketService ticketService;
@Test
void whenPostValidTicket_thenReturns201Created() throws Exception {
// Arrange
Ticket ticket = new Ticket();
ticket.setId(1L);
ticket.setTitle("Laptop won't start");
ticket.setStatus("OPEN");
ticket.setRequesterEmail("user@example.com");
when(ticketService.createTicket(any(Ticket.class))).thenReturn(ticket);
String ticketJson = "{\"title\":\"Laptop won't start\",\"status\":\"OPEN\",\"requesterEmail\":\"user@example.com\"}";
// Act & Assert
mockMvc.perform(post("/api/tickets")
.contentType(MediaType.APPLICATION_JSON)
.content(ticketJson))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.title").value("Laptop won't start"));
}
@Test
void whenPostInvalidTicket_thenReturns400BadRequest() throws Exception {
// Arrange: An empty JSON object is invalid because fields are non-nullable
String invalidTicketJson = "{\"title\":\"\"}"; // Assuming title cannot be blank
// Act & Assert
mockMvc.perform(post("/api/tickets")
.contentType(MediaType.APPLICATION_JSON)
.content(invalidTicketJson))
.andExpect(status().isBadRequest());
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
For more details on the available endpoints, see the API Reference.
Database Integration Tests
To test the JPA layer (entities and repositories), you can use the @DataJpaTest annotation. This slice test will:
- Scan for
@Entityclasses and Spring Data JPA repositories. - Disable full auto-configuration, focusing only on the persistence layer.
- Roll back transactions at the end of each test by default.
Note: To use @DataJpaTest with an in-memory database for testing, you need to add an H2 dependency to your pom.xml:
xml
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>1
2
3
4
5
2
3
4
5
Alternatively, tests can be configured to use a MySQL test instance or a Testcontainers-based MySQL container for more realistic database testing.
RabbitMQ Integration Tests
To test the AMQP integration, you can use @SpringBootTest to load the full context. For realistic RabbitMQ testing, consider using Testcontainers to run an actual RabbitMQ instance in a Docker container during tests. This approach provides high-fidelity integration testing without requiring a separate RabbitMQ installation.
Alternatively, you can mock the RabbitTemplate using @MockBean in integration tests to verify message publishing behavior without an actual broker.
Running Tests
Tests are a fundamental part of the build lifecycle and are executed automatically in a CI environment.
mvn test Command
To run all unit and integration tests locally, execute the following Maven command from the project root:
bash
# This command compiles the code and runs all tests in src/test/java
mvn test1
2
2
A successful run will end with a [INFO] BUILD SUCCESS message. If any tests fail, Maven will stop the build and provide a detailed report of the failures.
Test Coverage
While not configured by default in the provided pom.xml, integrating a code coverage tool like JaCoCo is highly recommended. It measures the percentage of code lines, branches, and methods executed by the test suite. This helps identify untested parts of the codebase.
To add JaCoCo, include the following plugin in your pom.xml:
xml
<!-- pom.xml build -> plugins section -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version> <!-- Use the latest version -->
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
After adding the plugin, running mvn test will generate a coverage report in target/site/jacoco/index.html.
Continuous Integration (CI)
All tests are designed to be executed within a CI pipeline (e.g., GitHub Actions, Jenkins). The standard command mvn clean verify is typically used, as it runs all tests and performs additional checks before packaging the application. A failing test will fail the CI build, preventing defective code from being merged or deployed.
Manual Testing
For local development, debugging, and exploratory testing, manual execution of API requests is highly effective. The project provides several ways to facilitate this. Before starting, ensure the application is running locally as described in Running Locally.
Using example-requests.http
The example-requests.http file in the project root contains a collection of sample HTTP requests for all API endpoints. This file can be used directly within IDEs like IntelliJ IDEA (Ultimate Edition) or Visual Studio Code (with the REST Client extension).
Example from example-requests.http:
http
### Create a new ticket
POST http://localhost:8080/api/tickets
Content-Type: application/json
{
"title": "Laptop won't start",
"description": "My laptop won't turn on after the latest Windows update. The power light blinks once but nothing appears on screen.",
"status": "OPEN",
"priority": "HIGH",
"requesterEmail": "john.doe@slalom.com",
"assignedTo": null
}
### Get all tickets
GET http://localhost:8080/api/tickets1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
To use, simply open the file in a compatible IDE and click the "run" icon next to each request.
Postman/Insomnia Collections
The cURL examples in the README.md or the requests in example-requests.http can be easily imported into API clients like Postman or Insomnia. These tools offer more advanced features like environment management, automated test suites, and request chaining, which are useful for more complex testing scenarios.
cURL Examples from README
The README.md provides cURL commands for each endpoint, which can be run directly from your terminal. This is a quick and scriptable way to interact with the API.
Example: Create a Ticket via cURL
bash
curl -X POST http://localhost:8080/api/tickets \
-H "Content-Type: application/json" \
-d '{
"title": "Laptop won''t start",
"description": "My laptop won''t turn on after the latest update",
"status": "OPEN",
"priority": "HIGH",
"requesterEmail": "user@example.com",
"assignedTo": "support@example.com"
}'1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10