Spring Boot์์ ํ ์คํธ ์ฝ๋ ์์ฑํ๊ธฐ: ๋จ์ ํ ์คํธ์ ํตํฉ ํ ์คํธ, MockMvc ํ์ฉ๋ฒ
Spring Boot ์ ํ๋ฆฌ์ผ์ด์
์์ ํ
์คํธ ์ฝ๋๋ ํ์์ ์ธ ์์์
๋๋ค. ํ
์คํธ๋ ์ฝ๋ ํ์ง์ ๋ณด์ฅ ํ๊ณ , ๊ฐ๋ฐ์๊ฐ ์ฝ๋ ๋ณ๊ฒฝ ์ ๋ฐ์ํ ์ ์๋ ์ค๋ฅ๋ฅผ ์กฐ๊ธฐ์ ๋ฐ๊ฒฌ ํ ์ ์๋๋ก ๋์์ค๋๋ค. ์ด๋ฒ ํฌ์คํ
์์๋ ๋จ์ ํ
์คํธ์ ํตํฉ ํ
์คํธ์ ์ฐจ์ด ,
@SpringBootTest
์ MockBean ์ฌ์ฉ๋ฒ , ๊ทธ๋ฆฌ๊ณ MockMvc๋ฅผ ํ์ฉํ REST API ํ
์คํธ ๋ฐฉ๋ฒ์ ๋ค๋ฃน๋๋ค.
๋ชฉ์ฐจ
- ๋จ์ ํ ์คํธ์ ํตํฉ ํ ์คํธ์ ์ฐจ์ด
- @SpringBootTest์ MockBean ํ์ฉ๋ฒ
- MockMvc๋ก REST API ํ ์คํธํ๊ธฐ
- ํ ์คํธ ์ฝ๋์ ์ค์์ฑ๊ณผ ํธ๋ ๋
- ํ ์คํธ ์์ ์ฝ๋์ ์คํ ๊ฒฐ๊ณผ
1. ๋จ์ ํ ์คํธ์ ํตํฉ ํ ์คํธ์ ์ฐจ์ด
ํ ์คํธ ์ฝ๋๋ ํฌ๊ฒ ๋จ์ ํ ์คํธ(Unit Test) ์ ํตํฉ ํ ์คํธ(Integration Test) ๋ก ๋๋ฉ๋๋ค. ์ด ๋ ๊ฐ์ง ํ ์คํธ์ ๋ชฉ์ ๊ณผ ๋ฒ์๋ฅผ ๋ช ํํ ์ดํดํ๊ณ ์ฌ์ฉํด์ผ ํจ์จ์ ์ธ ํ ์คํธ ์ฝ๋ ์์ฑ ์ด ๊ฐ๋ฅํฉ๋๋ค.
ํ ์คํธ ์ ํ | ๋จ์ ํ ์คํธ(Unit Test) | ํตํฉ ํ ์คํธ(Integration Test) |
---|---|---|
๋ฒ์ | ๊ฐ๋ณ ๋ฉ์๋๋ ํด๋์ค ํ ์คํธ | ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ๋ฌ ์ปดํฌ๋ํธ ๊ฐ ์ํธ์์ฉ ํ ์คํธ |
์๋ | ๋งค์ฐ ๋น ๋ฆ | ์๋์ ์ผ๋ก ๋๋ฆผ |
์์กด์ฑ | ์ธ๋ถ ์ปดํฌ๋ํธ์ ์์กดํ์ง ์์ (Mock ์ฌ์ฉ) | ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ์ธ๋ถ ์๋น์ค์ ์์กด |
์ฃผ์ ๋๊ตฌ | JUnit, Mockito | @SpringBootTest |
์์ | ํน์ ์๋น์ค ๋ฉ์๋๊ฐ ์ฌ๋ฐ๋ฅธ ๊ฐ์ ๋ฐํํ๋์ง ํ ์คํธ | ์ ์ฒด API ์์ฒญ์ ๋ํ ์๋ต ํ ์คํธ |
๊ฒฐ๋ก
- ๋จ์ ํ ์คํธ ๋ ๋น ๋ฅด๊ฒ ์ฝ๋๋ฅผ ๊ฒ์ฆํ๋ ๋ฐ ์ฌ์ฉ๋๊ณ , ํตํฉ ํ ์คํธ ๋ ์ค์ ํ๊ฒฝ์์ ๋ชจ๋ ๊ธฐ๋ฅ์ด ์ฌ๋ฐ๋ฅด๊ฒ ์๋ํ๋์ง ํ์ธ ํฉ๋๋ค. ๋ ๊ฐ์ง ํ ์คํธ๋ ์ํธ๋ณด์์ ์ด๋ฏ๋ก ํจ๊ป ์์ฑํ๋ ๊ฒ์ด ์ข์ต๋๋ค .
2. @SpringBootTest์ MockBean ํ์ฉ๋ฒ
@SpringBootTest
๋ ์ ํ๋ฆฌ์ผ์ด์
์ ๋ชจ๋ ์ปดํฌ๋ํธ ๋ฅผ ๋ก๋ํ์ฌ ํตํฉ ํ
์คํธ๋ฅผ ์ํํ ๋ ์ฌ์ฉ๋ฉ๋๋ค. MockBean ์ ์ธ๋ถ ์์กด์ฑ์ ๋ชจ์(Mock) ๊ฐ์ฒด๋ก ๋์ฒดํด ํ
์คํธ ํ๊ฒฝ์ ๋จ์ํํฉ๋๋ค.
์์ : UserService ํตํฉ ํ ์คํธ
User.java (์ํฐํฐ)
package com.example.demo.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Constructor, Getter, Setter ์๋ต
}
UserRepository.java (๋ ํฌ์งํ ๋ฆฌ)
package com.example.demo.repository;
import com.example.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
UserService.java (์๋น์ค ํด๋์ค)
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public Optional<User> getUserById(Long id) {
return userRepository.findById(id);
}
}
UserServiceTest.java (ํตํฉ ํ ์คํธ)
package com.example.demo;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import com.example.demo.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Test
public void ์ฌ์ฉ์_์กฐํ_ํ
์คํธ() {
// Given: ์ฌ์ฉ์ ์์ฑ
User user = new User();
user.setName("ํ๊ธธ๋");
userRepository.save(user);
// When: ์ฌ์ฉ์ ์กฐํ
User foundUser = userService.getUserById(user.getId()).orElse(null);
// Then: ๊ฒ์ฆ
assertThat(foundUser).isNotNull();
assertThat(foundUser.getName()).isEqualTo("ํ๊ธธ๋");
}
}
3. MockMvc๋ก REST API ํ ์คํธํ๊ธฐ
MockMvc ๋ ์คํ๋ง MVC์ HTTP ์์ฒญ๊ณผ ์๋ต์ ํ ์คํธ ํ ์ ์๋ ๋๊ตฌ์ ๋๋ค. ์ด๋ฅผ ํตํด ์ค์ ์๋ฒ๋ฅผ ์คํํ์ง ์๊ณ ๋ REST API์ ๋์์ ๊ฒ์ฆ ํ ์ ์์ต๋๋ค.
์์ : REST API ํ ์คํธ
UserController.java (REST API ์ปจํธ๋กค๋ฌ)
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.Optional;
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/users/{id}")
public Optional<User> getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
}
UserControllerTest.java (API ํ ์คํธ)
package com.example.demo;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.util.Optional;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
public class UserControllerTest {
private MockMvc mockMvc;
@Autowired
private UserController userController;
@MockBean
private UserService userService;
@Test
public void ์ฌ์ฉ์_API_ํ
์คํธ() throws Exception {
// Given: Mock ์ค์
User user = new User();
user.setId(1L);
user.setName("ํ๊ธธ๋");
when(userService.getUserById(1L)).thenReturn(Optional.of(user));
mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
// When & Then: API ํธ์ถ ๋ฐ ๊ฒ์ฆ
mockMvc.perform(get("/users/1")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("ํ๊ธธ๋"));
}
}
4. ํ ์คํธ ์ฝ๋์ ์ค์์ฑ๊ณผ ์ต์ ํธ๋ ๋
- CI/CD ํ์ดํ๋ผ์ธ์์ ํ์์ ์ธ ์ญํ : ๋ชจ๋ ์ฝ๋ ๋ณ๊ฒฝ ์ ํ ์คํธ๊ฐ ์๋์ผ๋ก ์คํ๋ฉ๋๋ค.
- ํ ์คํธ ์ฃผ๋ ๊ฐ๋ฐ(TDD) : ์ฝ๋๋ฅผ ์์ฑํ๊ธฐ ์ ์ ํ ์คํธ๋ฅผ ๋จผ์ ์์ฑํ๋ ๋ฐฉ๋ฒ๋ก ์ ๋๋ค.
- ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง ์ธก์ : ์ผ๋ง๋ ๋ง์ ์ฝ๋๊ฐ ํ ์คํธ๋๊ณ ์๋์ง๋ฅผ ํ์ธํ์ฌ ํ์ง์ ๋์ ๋๋ค.
- Mocking ๋๊ตฌ : ์ธ๋ถ API์์ ์์กด์ฑ์ ์ ๊ฑฐํ๊ธฐ ์ํด Mockito ์ ๊ฐ์ ๋๊ตฌ๋ฅผ ์ ๊ทน์ ์ผ๋ก ์ฌ์ฉํฉ๋๋ค.
๊ด๋ จ ๋งํฌ
Spring Boot ํตํฉ ํ ์คํธ ๊ฐ์ด๋๐
FAQ
1. ๋จ์ ํ ์คํธ์ ํตํฉ ํ ์คํธ๋ ์ธ์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข๋์?
- ๋จ์ ํ ์คํธ๋ ๊ฐ๋ณ ๋ฉ์๋์ ๋์ ์ ๊ฒ์ฆํ ๋, ํตํฉ ํ ์คํธ๋ ์ ์ฒด ๊ธฐ๋ฅ ๊ฐ ์ํธ์์ฉ ์ ํ์ธํ ๋ ์ฌ์ฉํฉ๋๋ค.
2. MockBean๊ณผ Mockito์ ์ฐจ์ด์ ์ ๋ฌด์์ธ๊ฐ์?
@MockBean
์ Spring ์ปจํ ์คํธ์ ์ฃผ์ ๋๋ ์์กด์ฑ์ ๋ชจ์ ํ๊ณ , Mockito๋ ํ ์คํธ ํด๋์ค ๋ด๋ถ์์ ๊ฐ์ฒด๋ฅผ ๋ชจ์ ํฉ๋๋ค.
3. ํตํฉ ํ ์คํธ๊ฐ ๋๋ฆฐ ์ด์ ๋ ๋ฌด์์ธ๊ฐ์?
- ํตํฉ ํ ์คํธ๋ ์ ์ฒด ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ก๋ ํ๊ณ , ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ์ธ๋ถ ์๋น์ค์์ ํต์ ์ด ํฌํจ๋ ์ ์์ด ๋๋ฆฝ๋๋ค.
4. MockMvc๋ก ๋น๋๊ธฐ ์์ฒญ์ ํ ์คํธํ ์ ์๋์?
- ๋ค, MockMvc ๋ ๋น๋๊ธฐ ์์ฒญ๋ ์ง์ํฉ๋๋ค.
.asyncDispatch()
๋ฉ์๋๋ฅผ ์ฌ์ฉํด ๋น๋๊ธฐ ๊ฒฐ๊ณผ๋ฅผ ๊ฒ์ฆํ ์ ์์ต๋๋ค.
5. ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ ๋ฌด์์ธ๊ฐ์?
- ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ ์ฝ๋์ ๋ช ํผ์ผํธ๊ฐ ํ ์คํธ๋๊ณ ์๋์ง๋ฅผ ์ธก์ ํ๋ ์งํ์ ๋๋ค. ๋์ ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ ์ฝ๋ ํ์ง์ ๋ณด์ฅํ๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค.
๋ง๋ฌด๋ฆฌ
์ด๋ฒ ํฌ์คํ ์์๋ Spring Boot์์ ํ ์คํธ ์ฝ๋ ์์ฑ ๋ฐฉ๋ฒ ์ ํ์ตํ์ต๋๋ค. ๋จ์ ํ ์คํธ์ ํตํฉ ํ ์คํธ์ ์ฐจ์ด์ ์ ์ดํดํ๊ณ , MockBean ๊ณผ MockMvc ๋ฅผ ํ์ฉํด REST API๋ฅผ ํ ์คํธํ๋ ๋ฐฉ๋ฒ๋ ํจ๊ป ๋ค๋ค์ต๋๋ค. ํ ์คํธ ์ฝ๋๋ ํ๋ก์ ํธ์ ํ์ง๊ณผ ์์ ์ฑ์ ๋ณด์ฅ ํ๋ฉฐ, ์ฝ๋ ๋ณ๊ฒฝ ์ ๋ฐ์ํ ์ ์๋ ์ค๋ฅ๋ฅผ ์กฐ๊ธฐ์ ๋ฐ๊ฒฌ ํ๋ ์ค์ํ ๋๊ตฌ์ ๋๋ค.
ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ ์ต๊ด์ ๋ค์ด๋ฉด, ์ฝ๋ ์ ์ง๋ณด์๊ฐ ์ฌ์์ง๊ณ ํ์ํฌ๊ฐ ํฅ์๋ฉ๋๋ค. ํนํ CI/CD ํ์ดํ๋ผ์ธ์์ ์๋ํ๋ ํ ์คํธ๋ ๋ฐฐํฌ ์ ์ ๋ฌธ์ ๋ฅผ ๋ฏธ๋ฆฌ ๋ฐ๊ฒฌ ํ๋ ๋ฐ ์ค์ํ ์ญํ ์ ํฉ๋๋ค.
์ด์ ์ด ๊ฐ์ด๋๋ฅผ ๋ฐํ์ผ๋ก ๋ ๋ง์ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๊ณ ์ฐ์ต ํด๋ณด์ธ์. ์์ ์ค์ต๋ค์ด ๋ชจ์ฌ ๋ ๋์ ๊ฐ๋ฐ์ ๋ก ์ฑ์ฅํ๋ ๋ฐ๊ฑฐ๋ฆ์ด ๋ ๊ฒ์ ๋๋ค. ๐
์ฐ์ต ๊ณผ์
- ์ฌ์ฉ์ ์์ฑ API ํ ์คํธ ๋ฅผ ์์ฑํด๋ณด์ธ์. (POST ์์ฒญ ์ฌ์ฉ)
- ์์ธ ์ฒ๋ฆฌ ์ํฉ์ ๋ํ ํ ์คํธ ์ฝ๋ ๋ฅผ ์์ฑํด๋ณด์ธ์. ์: ์๋ ์ฌ์ฉ์ ์กฐํ ์ 404 ์๋ต
- ๋น๋๊ธฐ ์์ฒญ ํ ์คํธ ๋ฅผ MockMvc๋ฅผ ์ฌ์ฉํด ๊ตฌํํด๋ณด์ธ์.
- ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์ธก์ ํด๋ณด๊ณ , ์ปค๋ฒ๋ฆฌ์ง ๋น์จ์ ๋์ฌ๋ณด์ธ์.
- CI ๋๊ตฌ(Jenkins, GitHub Actions) ์ ์ฐ๋ํด ํ ์คํธ ์๋ํ ํ๊ฒฝ์ ๊ตฌ์ถํด๋ณด์ธ์.
์ฌ๋ฌ๋ถ์ ํ ์คํธ ์ฝ๋ ์์ฑ ์ค๋ ฅ ์ด ํ๋ก์ ํธ์ ์ฑ๊ณต์ ๊ฒฐ์ ์ง๋ ์ค์ํ ์์๊ฐ ๋ฉ๋๋ค. ๊พธ์คํ ์ฐ์ต์ผ๋ก ์ฝ๋ ํ์ง์ ๋์ด๊ณ , ๋ ๋์ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฐ๋ฐํด๋ณด์ธ์! ๐
๋๊ธ