Hey there, developers! In this enhanced tutorial, we’ll build a Spring RESTful web service using Java 21 and Spring Boot 3, complete with an embedded PostgreSQL database. We’ll then create a client and use JUnit with Jersey Test to thoroughly test our service. Let’s get started!
Prerequisites
Before we begin, ensure you have the following installed:
- Java Development Kit (JDK): Version 21
- Maven: Version 3.9+ (or Gradle 8+)
- Your Favorite IDE: IntelliJ IDEA, Eclipse, VS Code, etc.
Part 1: Building the Spring RESTful Web Service (the Server)
We’ll start by setting up our server application with Spring Boot and an embedded PostgreSQL database.
Step 1: Create a New Spring Boot Project
Use Spring Initializr (https://start.spring.io/) to generate a new Spring Boot project. Include the following dependencies:
- Spring Web: For building RESTful web services.
- Spring Data JPA: For database interaction.
- PostgreSQL Driver: For connecting to PostgreSQL.
- Embedded PostgreSQL: For our embedded database.
Choose Maven as your build tool and Java 21.
Step 2: Configure pom.xml for Embedded PostgreSQL
Ensure your pom.xml
includes the necessary dependencies. Spring Boot 3 generally handles much of this, but double-check the embedded PostgreSQL dependency. Here’s an example:
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 |
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.7.2</version> </dependency> <dependency> <groupId>com.opentable.components</groupId> <artifactId>otj-pg-embedded</artifactId> <version>1.3.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.glassfish.jersey.test-framework.providers</groupId> <artifactId>jersey-test-framework-provider-grizzly2</artifactId> <version>2.27</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>21</source> <target>21</target> </configuration> </plugin> </plugins> </build> |
Step 3: Define the Entity
Let’s create a Greeting
entity that will be stored in our database:
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 |
package com.example.demo.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity public class Greeting { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String message; public Greeting() {} // Required for JPA public Greeting(String message) { this.message = message; } // Getters and setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } |
Step 4: Create a Repository
Create a repository to interact with the database:
1 2 3 4 5 6 7 8 9 10 |
package com.example.demo.repository; import com.example.demo.model.Greeting; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface GreetingRepository extends JpaRepository<Greeting, Long> { } |
Step 5: Create the REST Controller
Now, let’s create our REST controller:
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 |
package com.example.demo.controller; import com.example.demo.model.Greeting; import com.example.demo.repository.GreetingRepository; import org.springframework.web.bind.annotation.*; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; @RestController @RequestMapping("/greetings") public class GreetingController { @Autowired private GreetingRepository greetingRepository; @GetMapping public List<Greeting> getAllGreetings() { return greetingRepository.findAll(); } @GetMapping("/{id}") public Greeting getGreetingById(@PathVariable Long id) { return greetingRepository.findById(id).orElse(null); // Simplified null handling } @PostMapping public Greeting createGreeting(@RequestBody Greeting greeting) { return greetingRepository.save(greeting); } } |
In this controller:
- We use
@Autowired
to inject theGreetingRepository
. @GetMapping
and@PostMapping
handle GET and POST requests.- We’re interacting with the database using JPA.
Step 6: Application Configuration (Optional)
For Spring Boot 3, the default settings often suffice. If you need specific database configurations, you can use application.properties
or application.yml
. For example, to set a datasource name (though the embedded DB doesn’t strictly need this):
1 2 |
spring.datasource.name=my_embedded_db |
Part 2: Testing with JUnit and Jersey Test
Now, let’s write a JUnit test using Jersey Test to verify our REST service.
Step 1: Create a Test Class
Create a new test class, e.g., GreetingControllerTest
:
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 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
package com.example.demo.controller; import com.example.demo.model.Greeting; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.glassfish.jersey.server.ApplicationHandler; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.DeploymentContext; import org.glassfish.jersey.test.JerseyTest; import org.glassfish.jersey.test.TestProperties; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.test.context.TestPropertySource; import org.springframework.web.context.WebApplicationContext; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import java.util.List; import java.util.ArrayList; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // Use random port to avoid conflicts @AutoConfigureMockMvc @TestPropertySource(properties = { "spring.datasource.url=jdbc:h2:mem:testdb", "spring.datasource.driver-class-name=org.h2.Driver" }) // Use H2 for testing public class GreetingControllerTest { @LocalServerPort private int port; private WebTarget target; private Client c; @Autowired private WebApplicationContext context; private ObjectMapper objectMapper; @BeforeEach public void setUp() throws Exception { this.c = ClientBuilder.newClient(); this.target = c.target("http://localhost:" + port).path("greetings"); objectMapper = new ObjectMapper(); } @AfterEach public void tearDown() throws Exception { if (c != null) { c.close(); } } @Test public void testGetAllGreetings() throws Exception { // Create a greeting first using POST Greeting newGreeting = new Greeting("Hello, Test!"); Response postResponse = target.request(MediaType.APPLICATION_JSON_TYPE) .post(jakarta.ws.rs.client.Entity.entity(objectMapper.writeValueAsString(newGreeting), MediaType.APPLICATION_JSON_TYPE)); assertEquals(HttpStatus.CREATED.value(), postResponse.getStatus()); // Then get all greetings Response response = target.request().get(); assertEquals(HttpStatus.OK.value(), response.getStatus()); String json = response.readEntity(String.class); List<Greeting> greetings = objectMapper.readValue(json, objectMapper.getTypeFactory().constructCollectionType(List.class, Greeting.class)); assertNotNull(greetings); assertEquals(1, greetings.size()); // Assuming this is the first test and the database is empty assertEquals("Hello, Test!", greetings.get(0).getMessage()); } @Test public void testGetGreetingById() throws Exception { // First create a greeting Greeting newGreeting = new Greeting("Greeting for ID test"); Response postResponse = target.request(MediaType.APPLICATION_JSON_TYPE) .post(jakarta.ws.rs.client.Entity.entity(objectMapper.writeValueAsString(newGreeting), MediaType.APPLICATION_JSON_TYPE)); assertEquals(HttpStatus.CREATED.value(), postResponse.getStatus()); String location = postResponse.getLocation().toString(); Long id = Long.parseLong(location.substring(location.lastIndexOf("/") + 1)); // Now retrieve it by ID Response response = target.path(String.valueOf(id)).request().get(); assertEquals(HttpStatus.OK.value(), response.getStatus()); String json = response.readEntity(String.class); Greeting greeting = objectMapper.readValue(json, Greeting.class); assertNotNull(greeting); assertEquals("Greeting for ID test", greeting.getMessage()); } @Test public void testCreateGreeting() throws Exception { Greeting greeting = new Greeting("Hello, POST test!"); Response response = target.request(MediaType.APPLICATION_JSON_TYPE) .post(jakarta.ws.rs.client.Entity.entity(objectMapper.writeValueAsString(greeting), MediaType.APPLICATION_JSON_TYPE)); assertEquals(HttpStatus.CREATED.value(), response.getStatus()); // Use CREATED for POST assertNotNull(response.getLocation()); String json = response.readEntity(String.class); Greeting createdGreeting = objectMapper.readValue(json, Greeting.class); assertEquals("Hello, POST test!", createdGreeting.getMessage()); } } |
Explanation:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
: Starts the application on a random port to avoid conflicts during testing.@AutoConfigureMockMvc
: We don’t use MockMvc here, but it’s often helpful in Spring Boot tests, and including it doesn’t hurt.@TestPropertySource
: Overrides the default database configuration to use an in-memory H2 database for testing. This is generally preferred over an embedded Postgres for unit tests, as H2 is faster and lighter-weight. We’ll keep the embedded Postgres for demonstrating the full setup.@LocalServerPort
: Injects the port the application is running on.Client
andWebTarget
: These are Jersey classes for making HTTP requests.ObjectMapper
: Used to convert between Java objects and JSON.- The
@Test
methods send requests to the endpoints and assert the responses. We usepost()
to create new greetings before retrieving them, ensuring our tests are self-contained and don’t rely on pre-existing data. - We’re testing the status codes and the content of the responses.
Step 2: Run the Tests
Run the GreetingControllerTest
class as a JUnit test. It will start your Spring Boot application, execute the tests, and then shut down the application. All tests should pass.
Part 3: Running the Application
To run the Spring Boot application:
- Build the project:
mvn clean install
- Run the application:
mvn spring-boot:run
The application will start, and you can access the endpoints (e.g., using curl
or a browser):
curl http://localhost:8080/greetings
curl -X POST -H "Content-Type: application/json" -d '{"message":"Hello from curl"}' http://localhost:8080/greetings
curl http://localhost:8080/greetings/1
(if you’ve created a greeting with ID 1)
Conclusion
In this tutorial, we’ve built a robust Spring RESTful web service using Java 21 and Spring Boot 3, incorporating an embedded PostgreSQL database for persistence. We’ve also implemented comprehensive testing using JUnit and Jersey Test. This approach ensures that your service is not only functional but also reliable and well-tested. Remember to adapt this code to your specific needs and explore the advanced features of Spring Boot and Jersey.