CodeGym /Courses /Module 5. Spring /Lecture 137: Practice: writing integration tests for a RE...

Lecture 137: Practice: writing integration tests for a REST API

Module 5. Spring
Level 9 , Lesson 6
Available

Integration tests verify how multiple components of the application interact. If Unit tests study each component in isolation, here we want to know whether they work correctly together. For example, can we fetch data from the database via a controller, are requests handled properly, and do services communicate with each other correctly.

Why are they important?

  • You check the actual interaction between components. A REST API without a working service layer is just decoration, right?
  • Confidence in functionality: integration tests help you make sure your app does what it's supposed to do.
  • Catching configuration issues: often problems aren't in the code but in component settings (for example, database connection).

How to prepare for integration tests?

Since we'll be interacting with all layers of our app, we need to set up the following:

  1. Test database. For isolation we'll use an in-memory database (for example, H2).
  2. Start the whole Spring context. The point of integration tests is to test the app as if it's running in a real environment.
  3. Appropriate annotations. We use the @SpringBootTest annotation to load the entire application context.

Environment setup

Creating a test database

In application.properties we'll add settings for connecting to H2. This lets our tests run without depending on a real database:


spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop

These settings will create a database on the fly that will be dropped after the tests finish.


Writing integration tests

Let's consider an app that works with the User entity. We have a REST API that allows CRUD operations (create, read, update, delete) for users.

Example of our User entity


@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String email;

    // Getters and setters...
}

Repository for database access


@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

Controller to handle requests


@RestController
@RequestMapping("/users")
public class UserController {
    private final UserRepository userRepository;

    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody User user) {
        User savedUser = userRepository.save(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        return userRepository.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    // Other CRUD methods...
}

Test class

For integration testing we'll create a test class. We'll check interaction between the controller and the repository.

Steps:

  1. Use the @SpringBootTest annotation to launch the Spring context.
  2. Use MockMvc to simulate HTTP requests and verify responses.

@SpringBootTest
@AutoConfigureMockMvc
class UserIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private UserRepository userRepository;

    @BeforeEach
    void cleanDatabase() {
        userRepository.deleteAll();
    }

    @Test
    void shouldCreateUser() throws Exception {
        // Create User object to send
        User user = new User();
        user.setName("John Doe");
        user.setEmail("john.doe@example.com");

        // Perform POST request
        mockMvc.perform(post("/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(user)))
                .andExpect(status().isCreated()) // Check that status is 201 Created
                .andExpect(jsonPath("$.id").exists()) // Check that response contains ID
                .andExpect(jsonPath("$.name").value("John Doe")) // Check name
                .andExpect(jsonPath("$.email").value("john.doe@example.com")); // Check email
    }

    @Test
    void shouldRetrieveUserById() throws Exception {
        // Save a user to the database
        User user = new User();
        user.setName("Jane Doe");
        user.setEmail("jane.doe@example.com");
        user = userRepository.save(user);

        // Perform GET request
        mockMvc.perform(get("/users/{id}", user.getId())
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk()) // Check that status is 200 OK
                .andExpect(jsonPath("$.id").value(user.getId()))
                .andExpect(jsonPath("$.name").value("Jane Doe"))
                .andExpect(jsonPath("$.email").value("jane.doe@example.com"));
    }

    @Test
    void shouldReturnNotFoundWhenUserDoesNotExist() throws Exception {
        // Perform GET request with a non-existing ID
        mockMvc.perform(get("/users/{id}", 999)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isNotFound()); // Check that status is 404
    }
}
  1. The @SpringBootTest annotation loads the entire application context, including the database.
  2. The @AutoConfigureMockMvc annotation sets up MockMvc to simulate HTTP requests.
  3. In the shouldCreateUser test we send a POST request to create a new user and verify the server responds with code 201 and returns correct data.
  4. In the shouldRetrieveUserById test we check that an existing user can be found by its ID.
  5. In the shouldReturnNotFoundWhenUserDoesNotExist test we verify that a request to a non-existing resource returns status 404.

Common mistakes and tips

If you've ever written tests, you probably know that "If something can go wrong, it will". Here's how to minimize issues:

  • Problem: the database contains "junk". If previous tests leave data in the database, it can affect the results of following tests. Always clean the database before each test (like in the cleanDatabase method in the example).
  • Database configuration error. Make sure the test database is actually used for tests and not your main database. Check application.properties!
  • Problems with MockMvc. If you forgot the @AutoConfigureMockMvc annotation, your MockMvc-based tests won't work.
  • Check error messages. When something goes wrong, study the stack trace — often the issue is a tiny detail.

Now you're equipped with the essentials for writing integration tests for REST APIs. You can use this practice in real projects to make sure your app meets requirements and behaves as intended. The example above is also useful for interview prep, where you're often asked to give examples of tests.

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION