๐ ๋ชฉ์ฐจ
- JUnit์ด๋?
- JUnit ์ ์ฉ ๋ฐฉ๋ฒ
- JUnit ์ ์ฉ ์์
- DB ์ฐ๋ ๋จ์ ํ ์คํธ ๋ฐฉ๋ฒ
- ํ ์คํธ ์คํ
- ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๋?
- ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง ์ธก์ ๋ฐฉ๋ฒ - JaCoCo
- ํ๊ธ ๊ฒฝ๋ก ๋ฌธ์ ํด๊ฒฐ ๋ฐฉ๋ฒ
- ํ
์คํธ ๊ฒฐ๊ณผ ํ์ธ
๐ฏ 0.JUnit ๋จ์ ํ ์คํธ ์ ์ฉํ๊ธฐ
๊ธฐ์กด ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ์ง ์๊ณ ์ง์ ํ๋ก๊ทธ๋จ์ ๋ก์ปฌ์์ ์คํํด์ ํ ์คํธํ๊ณ ์์๋ค.
=> ์ฝ๋ ๋ณ๊ฒฝ ์ ๋ง๋ค ๊ธฐ๋ฅ์ ์ฌํ ์คํธ ํ๋๋ฐ ์๊ฐ์ด ์์๋๊ณ , ํ ์คํฐ๊ฐ ๋์น๋ ๋ถ๋ถ์ด ์์ ์ ์์.
์ด๋ฅผ ๊ฐ์ ํ๊ธฐ ์ํด JUnit ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ์ ์์๋ณด๊ณ ‘์ผ์ ์์คํ ’์ ์ ์ฉํด๋ณด๊ธฐ๋ก ํ๋ค!!
๐ 1. JUnit์ด๋?
- Java์์ ๊ฐ์ฅ ๋ง์ด ์ฐ์ด๋ ๋จ์ ํ ์คํธ ํ๋ ์์ํฌ
- ์ญํ : ํ ์คํธ ์์ฑ → ์๋ ์คํ → ๊ฒฐ๊ณผ ํ์ธ
- Spring Boot์์๋ spring-boot-starter-test์ JUnit 5, Mockito๊ฐ ํฌํจ๋์ด ์๋ค!!
๐ pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- <dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency> -->
<!-- Mockito -->
<!-- <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency> -->
โ ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
- ํ ์คํธ ํด๋์ค: src/test/java
- ํ ์คํธ ๋ฉ์๋: @Test ๋ถ์
๐ ์์
@SpringBootTest
class MyServiceTest {
@Autowired
MyService myService;
@Test
void testAdd() {
int result = myService.add(2, 3);
assertEquals(5, result);
}
}
๐ 2. JUnit ์ ์ฉ ๋ฐฉ๋ฒ
โ 2-1. ํ ์คํธ ํด๋์ค ์ ํ
๐น๊ธฐ๋ณธ ์์น: ๋ณดํต ํ๋์ ํด๋์ค → ํ๋์ ํ
์คํธ ํด๋์ค
๐ ์: ScheduleServiceImpl.java → ScheduleServiceImplTest.java
๐น๊ท์น
- ํ ์คํธ ํด๋์ค ์ด๋ฆ์ ๋ณดํต ์๋ณธ ํด๋์ค ์ด๋ฆ + Test
- ํจํค์ง ๊ตฌ์กฐ๋ ์๋ณธ ํด๋์ค์ ๊ฐ์ ํจํค์ง์ ๋๋ฉด private ๋ฉ์๋ ํ ์คํธ๋ package-private ์ ๊ทผ์ด ํธํจ
๐ ์
src/main/java/kr/co/calender/service/ScheduleServiceImpl.java
src/test/java/kr/co/calender/service/ScheduleServiceImplTest.java
๐น์์ธ
- ์ฌ๋ฌ ๊ด๋ จ ๊ธฐ๋ฅ์ ๋ฌถ์ด์ ํตํฉ ํ ์คํธ๋ฅผ ํ๊ณ ์ถ์ผ๋ฉด → ํ ํ์ผ์ ์ฌ๋ฌ ํด๋์ค๋ฅผ ํ ์คํธํ ์๋ ์์
- ๋ฐ๋๋ก ๋จ์ helper ์ ํธ์ด๋ผ๋ฉด → ์ฌ๋ฌ ์ ํธ์ ํ ํ
์คํธ ํ์ผ์์ ๋ค๋ค๋ ๋จ
๐น ๐ ๊ฒฐ๋ก
๋๋ถ๋ถ์ 1:1 ๋งค์นญ, ํ์์ ํตํฉ ํ ์คํธ์ฉ ํ์ผ ๋ฐ๋ก ์์ฑ
โ 2-2. ํ ์คํธํ ๋ฉ์๋ ์ ํ
๐น๊ณต๊ฐ(Public) ๋ฉ์๋ ์์ฃผ๋ก ํ ์คํธ
- Controller → API ํธ์ถ ๊ฒฐ๊ณผ ๊ฒ์ฆ
- Service → ํต์ฌ ๋ก์ง(๊ณ์ฐ, ์ํ ๋ณ๊ฒฝ, ์์ธ ์ฒ๋ฆฌ) ๊ฒ์ฆ
๐นprivate ๋ฉ์๋
- ์ผ๋ฐ์ ์ผ๋ก ์ง์ ํ ์คํธํ์ง ์๊ณ public ๋ฉ์๋๋ฅผ ํตํด ๊ฐ์ ํ ์คํธ
- ์์ธ: ๊ผญ ํ์ํ๋ฉด Reflection์ผ๋ก ํธ์ถ ๊ฐ๋ฅ(Method.invoke())
โ 2-3. given-when-then ํจํด
- given-when-then ํจํด (BDD – Behavior Driven Development ์คํ์ผ)
- ํ ์คํธ ์ฝ๋์์ ์กฐ๊ฑด → ์คํ → ๊ฒฐ๊ณผ๋ฅผ ๊ตฌ๋ถํด ๊ฐ๋ ์ฑ์ ๋์ด๋ ํจํด
1๏ธโฃ Given : ํ
์คํธ ์ค๋น (์
๋ ฅ๊ฐ, Mock ๋์ ์ค์ )
2๏ธโฃ When : ์ค์ ํ ์คํธ ๋์ ๋ฉ์๋ ํธ์ถ
3๏ธโฃ Then : ์คํ ๊ฒฐ๊ณผ ๊ฒ์ฆ (assert, verify)
๐ ์์
@Test
@DisplayName("์ฃผ๋ฌธ ์์ฑ ์ฑ๊ณต")
void placeOrder_success() {
// Given (์ํ/Mock ์ค๋น)
when(orderRepository.save(any())).thenReturn(new Order(1L));
// When (๋ฉ์๋ ์คํ)
Order saved = orderService.placeOrder(new CreateOrderCmd(...));
// Then (๊ฒ์ฆ)
assertThat(saved.getId()).isEqualTo(1L);
verify(orderRepository, times(1)).save(any());
}
โ 2-4. ์์ฃผ ์ฐ๋ JUnit ์ด๋ ธํ ์ด์
| ์ด๋ ธํ ์ด์ | ์ฉ๋ |
| @Test | ํ ์คํธ ๋ฉ์๋ ์ง์ |
| @BeforeEach | ๊ฐ ํ ์คํธ ์คํ ์ ๋ง๋ค ์ด๊ธฐํ ์์ ์ํ |
| @AfterEach | ๊ฐ ํ ์คํธ ์คํ ํ๋ง๋ค ์ ๋ฆฌ ์์ ์ํ |
| @BeforeAll | ํ ์คํธ ํด๋์ค ์์ ์ ํ ๋ฒ ์คํ (static) |
| @AfterAll | ํ ์คํธ ํด๋์ค ์ข ๋ฃ ํ ํ ๋ฒ ์คํ (static) |
| @Disabled | ํน์ ํ ์คํธ ์์๋ก ์ ์ธ |
| @DisplayName("์ค๋ช ") | ํ ์คํธ ์ด๋ฆ์ ์ค๋ช ์ถ๊ฐ ๊ฐ๋ฅ |
โ 2-5. Mock ๊ฐ์ฒด ์ฌ์ฉ
๐ 2-5-1. Mock์ด๋?
- ๊ฐ์ง ํ๋ ฅ์ ๊ฐ์ฒด
- ํธ์ถ ์ ๋ฐํ๊ฐ์ ์ง์ ํ ์ ์๊ณ , ํธ์ถ ์ฌ๋ถ/ํ์/ํ๋ผ๋ฏธํฐ๋ฅผ ๊ฒ์ฆํ ์ ์์
๐ 2-5-2. Mockito๋?
- ๊ฐ์ฅ ๋ง์ด ์ฐ์ด๋ ์๋ฐ Mocking ํ๋ ์์ํฌ
- ํ ์คํธ ๋์ ํด๋์ค์ ํ๋ ฅ ๊ฐ์ฒด๋ฅผ ๊ฐ์ง(Mock)๋ก ์์ฑํ๊ณ , ๋์์ ์ ์ํ๊ฑฐ๋ ํธ์ถ์ ๊ฒ์ฆํ ์ ์๊ฒ ํด์ค
- JUnit๊ณผ ํจ๊ป ์ฐ๋ฉด ๋จ์ ํ ์คํธ ์์ฑ์ด ํจ์ฌ ๊ฐ๋จํด์ง
๐ ์์
// Mock ์์ฑ
UserRepository repo = mock(UserRepository.class);
// ํ๋ ์ ์ (Stub)
when(repo.findById(1L)).thenReturn(new User(1L, "ํ๊ธธ๋"));
// ํธ์ถ & ๊ฒฐ๊ณผ ๊ฒ์ฆ
verify(repo).findById(1L);
assertEquals(3, result.getUsersList().size());
๐ *BDD ์คํ์ผ (BDDMockito) - when๊ณผ ๋์ผ ๊ธฐ๋ฅ, ๋ค๋ฅธ ์คํ์ผ
given(repo.findById(1L)).willReturn(new User(1L, "ํ๊ธธ๋"));
๐น2-5-3. @MockBean vs @Mock vs @InjectMocks
- @Mock: ๊ฐ์ง ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด ๋ฐํํด์ฃผ๋ ์ด๋ ธํ ์ด์ -> ํ ์คํธ ๋์ ๋ด๋ถ์์ ์ฌ์ฉํ๋ ํด๋์ค
- @Spy: Stubํ์ง ์์ ๋ฉ์๋๋ค์ ์๋ณธ ๋ฉ์๋ ๊ทธ๋๋ก ์ฌ์ฉํ๋ ์ด๋ ธํ ์ด์
- @InjectMocks: ํ ์คํธ ๋์ ํด๋์ค ์์ฑ ๋ฐ Mock(Mock/Spy๋ก ์์ฑ๋ ๊ฐ์ง ๊ฐ์ฒด) ์ฃผ์ -> ํ ์คํธ ๋์ ํด๋์ค
- *@MockBean : Spring ์ปจํ ์คํธ์์ ์ค์ Bean ๋์ฒด (Spring boot ํ๊ฒฝ, ํตํฉ ํ ์คํธ์ฉ)
๐ก ํ
- @InjectMocks → ๊ตฌํ์ฒด ๋์์ผ๋ก๋ง ํ ์คํธ (์ธํฐํ์ด์ค๋ ๋ก์ง ์์)
- @MockBean → ์ธํฐํ์ด์ค ํ์ ์ผ๋ก ๋ฑ๋กํ๋ ๊ฒ ์ผ๋ฐ์
๐น 2-4-4. ์ธ์ ๋ญ ์ฐ์ง?
๐ ์๋น์ค ๋จ์ ํ ์คํธ (Repository๋ง Mock)
์๋น์ค ๋ฉ์๋ ๋ด์์ ์คํ๋๋ Repository, Mapper๋ฅผ Mock์ผ๋ก ๋ฑ๋ก, .thenReturn()์ผ๋ก ๊ฐ์ง์ ํ์ฌ ์ค์ DBํธ์ถ ์์ด ํ ์คํธํ๋ค.
-> @Mock ์ด๋ ธํ ์ด์ ์ ํตํด ๊ฐ์ง UserRepository๋ฅผ ๋ง๋ค๊ณ , @InjectMocks๋ฅผ ํตํด User Service ์ ์ด๋ฅผ ์ฃผ์
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock private OrderRepository orderRepository; // ์ปจํ
์คํธ ๋ฑ๋ก X
@InjectMocks private OrderServiceImpl orderService; // ํ
์คํธ ๋์
@Test
void placeOrder_success() {
when(orderRepository.save(any())).thenReturn(new Order(1L));
Order saved = orderService.placeOrder(new CreateOrderCmd(...));
assertThat(saved.getId()).isEqualTo(1L);
verify(orderRepository, times(1)).save(any());
}
}
- ๋น ๋ฆ, ์คํ๋ง ์ ๋์ → ์์ ๋น์ฆ๋์ค ๋ก์ง ๊ฒ์ฆ์ ์ต์ .
๐ ์ปจํธ๋กค๋ฌ ์ฌ๋ผ์ด์ค ํ ์คํธ (์๋น์ค๋ง Mock) → @WebMvcTest + @MockBean
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired private MockMvc mvc;
@MockBean private OrderService orderService; // ์ปจํ
์คํธ์ "์๋น์ค ๋น"์ผ๋ก ๋ฑ๋ก & ์ค์ ๋น ๋์ฒด
@Test
void getOrder_returns200() throws Exception {
when(orderService.get(1L)).thenReturn(new OrderDto(1L, "OK"));
mvc.perform(get("/orders/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1L));
verify(orderService).get(1L);
}
}
- MVC ๋น๋ง ๋ก๋ฉ → ์ปจํธ๋กค๋ฌ-์๋น์ค ๊ฒฝ๊ณ ๊ฒ์ฆ์ ์ ํฉ
โ 2-5. Assert ์ฌ์ฉ
JUnit์์ ๊ฒ์ฆ(assertion) ์ฉ๋๋ก ์ฌ์ฉ
- assertEquals(expected, actual) : ๊ฐ ๋น๊ต
- assertNotNull(object) : null ์ฌ๋ถ ํ์ธ
- assertTrue(condition) : ์กฐ๊ฑด true ํ์ธ
- assertFalse(condition) : ์กฐ๊ฑด false ํ์ธ
- assertThrows(Exception.class, () -> method()) : ์์ธ ๋ฐ์ ๊ฒ์ฆ
- assertAll(executables...) : ๋ชจ๋ assertion์ ์คํ, ์ผ๋ถ ์คํจํด๋ ๋๋จธ์ง assertion ์คํ
assertAll(
() -> assertEquals(1, result),
() -> assertNotNull(obj),
() -> verify(scheduleMapper).selectSchedule("SCHD001"),
);
โ 2-6. verify
- ํด๋น Mock ๊ฐ์ฒด์ ํน์ ๋ฉ์๋๊ฐ ์ค์ ๋ก ํธ์ถ๋๋์ง ํ์ธ
๐ ๊ธฐ๋ณธ ์ฌ์ฉ
verify(scheduleMapper).updateSchedule(any(ScheduleDTO.class));
๐ ํ์ ์ ์ด
- times(n) : n๋ฒ ํธ์ถ๋๋์ง ํ์ธ
- never() : ํธ์ถ๋์ง ์์๋์ง ํ์ธ
- atLeast(n) / atMost(n) : ์ต์/์ต๋ ํธ์ถ ํ์ ํ์ธ
verify(scheduleMapper, times(1)).updateSchedule(any());
verify(scheduleMapper, never()).deleteScheduleSharedUsers("SCHD002");
verify(scheduleMapper, atLeast(2)).insertScheduleSharedUser(any());
verify(scheduleMapper, atMost(3)).selectSchedule("SCHD001");
๐ ์์ ์ ์ด (InOrder)
InOrder inOrder = inOrder(scheduleMapper);
inOrder.verify(scheduleMapper).selectSchedule("SCHD001");
inOrder.verify(scheduleMapper).deleteScheduleSharedUsers("SCHD001");
inOrder.verify(scheduleMapper).insertScheduleSharedUser(any());
โ 2-6. Reflection ์ฌ์ฉ (private ๋ฉ์๋ ํ ์คํธ)
- ๋ณดํต์ public ๋ฉ์๋๋ฅผ ํตํด ๊ฐ์ ๊ฒ์ฆํ๋ ๊ฒ ์์น
- ๊ผญ ํ์ํ ๋๋ง ์ฌ์ฉ
Method privateMethod = MyService.class.getDeclaredMethod("privateCalculate", int.class);
privateMethod.setAccessible(true); // ์ ๊ทผ ์ ํ ํด์
int result = (int) privateMethod.invoke(myService, 10); // ๋ฉ์๋ ํธ์ถ
assertEquals(20, result);
๐ 3. JUnit ์ ์ฉ ์์
๐ก Tip
- ํต์ฌ ๋ก์ง, ์์ธ ์ฒ๋ฆฌ, ๋ถ๊ธฐ(if/else) ์์ฃผ๋ก ํ ์คํธ
- ์ปค๋ฒ๋ฆฌ์ง ์์น๋ฅผ ๋์ด๋ ๊ฒ๋ณด๋ค ์ค์ ๋ก์ง ๊ฒ์ฆ์ด ๋ ์ค์
- DB ์ฐ๋์ Mock ๋๋ ์ธ๋ฉ๋ชจ๋ฆฌ DB๋ฅผ ํ์ฉ
โ 3-1. Controller/Service ํ ์คํธ
- Spring Boot ํ
์คํธ ํ๊ฒฝ์์ ํธ์ถ ํ
์คํธ ๊ฐ๋ฅ
// 1๏ธโฃ Spring Boot ํ
์คํธ ํ๊ฒฝ์์ Controller๋ฅผ ํ
์คํธํ ๋ ์ฌ์ฉ
// webEnvironment = RANDOM_PORT : ์ค์ ์๋ฒ ํฌํธ๊ฐ ๋๋ค์ผ๋ก ํ ๋น๋์ด ํ
์คํธ ๊ฐ ์ถฉ๋ ๋ฐฉ์ง
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// 2๏ธโฃ MockMvc๋ฅผ ์๋ ์ค์ ํด์ HTTP ์์ฒญ/์๋ต์ ํ
์คํธํ ์ ์๊ฒ ํจ
@AutoConfigureMockMvc
// 3๏ธโฃ ํ
์คํธ์ฉ Security ์ค์ ์ ๋ถ๋ฌ์์ ์ธ์ฆ/์ธ๊ฐ ๋ฌธ์ ์์ด ํ
์คํธ ๊ฐ๋ฅ
@Import(TestSecurityConfig.class)
class ScheduleRestControllerTest {
// 4๏ธโฃ MockMvc ๊ฐ์ฒด ์ฃผ์
→ Controller ์์ฒญ ํ
์คํธ์ฉ
@Autowired
private MockMvc mockMvc;
// 5๏ธโฃ ์ค์ Service ํธ์ถ์ Mock ์ฒ๋ฆฌ → DB๋ ๋ค๋ฅธ ์์กด์ฑ ์ํฅ ์์ด ํ
์คํธ ๊ฐ๋ฅ
@MockBean
private ScheduleService scheduleService;
// 6๏ธโฃ ํ
์คํธ ์์ ์ ์ด๊ธฐํ ์์
@BeforeEach
void setUp() {
// ์: Mock ๋ฐํ๊ฐ ์ค์
List<ScheduleDTO> mockSchedules = new ArrayList<>();
mockSchedules.add(new ScheduleDTO("2025-08-18", "ํ
์คํธ ์ผ์ "));
// scheduleService.getSchedules() ํธ์ถ ์ mockSchedules ๋ฐํ
Mockito.when(scheduleService.getSchedules()).thenReturn(mockSchedules);
}
// 7๏ธโฃ ์ค์ API ํธ์ถ ํ
์คํธ
@Test
void testGetSchedules() throws Exception {
mockMvc.perform(get("/api/schedules")) // GET ์์ฒญ ์ํ
.andExpect(status().isOk()) // HTTP ์ํ 200 OK ํ์ธ
.andExpect(jsonPath("$").isArray()) // JSON ๋ฐฐ์ด ํ์์ธ์ง ํ์ธ
.andExpect(jsonPath("$[0].title").value("ํ
์คํธ ์ผ์ ")); // ์ฒซ ๋ฒ์งธ ์์ ์ ๋ชฉ ํ์ธ
}
}
์ด๋ ๊ฒ ํ๋ฉด Controller → Service → Repository ํธ์ถ ํ๋ฆ์ ์ ์ฒด์ ์ผ๋ก ํ ์คํธ ๊ฐ๋ฅ
โ 3-2. Spring Security ํ ์คํธ
- ์ด์ ํ๊ฒฝ์์๋ Security ๋๋ฌธ์ ํ
์คํธ ์คํจ ๊ฐ๋ฅ → ๋จ์ ํ
์คํธ์ฉ SecurityConfig ์ถ๊ฐ
@TestConfiguration
@EnableWebSecurity
public class TestSecurityConfig {
@Bean
public SecurityFilterChain testFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll()
);
return http.build();
}
}
- ํ
์คํธ ์ Security๋ฅผ ์์ ํ ์ฐํ → MockMvc๋ก Controller ํ
์คํธ ๊ฐ๋ฅ
โ 3-3. Private ๋ฉ์๋ ํ ์คํธ
- ๋ณดํต Private ๋ฉ์๋๋ ์ง์ ํ ์คํธํ์ง ์๊ณ public ๋ฉ์๋ ํธ์ถ ์ ๊ฐ์ ํ ์คํธ
- ๋จ, ํต์ฌ ๋ก์ง์ด private์ด๋ฉด Reflection์ผ๋ก ํ ์คํธ ๊ฐ๋ฅ
๐ ํต์ฌ ๋ก์ง ํ๋จ ์์ - ์ด๋ค ๋ฉ์๋๋ฅผ ํ ์คํธํ๋ ๊ฒ ์ข์๊นโ
| ๋ฉ์๋ | ํ ์คํธ ํ์์ฑ | ์ด์ |
| generateRepeatedSchedules | โญโญโญโญโญ (ํ์) | ๋ฐ๋ณต ์ฃผ๊ธฐ๋ณ ๋ ์ง ๊ณ์ฐ์ด ๋ฌ๋ผ์ ๋ฒ๊ทธ ๊ฐ๋ฅ์ฑ ํผ. ์๋ณธ์ผ์ ์ฒ๋ฆฌ์ ์๋ชป๋ ์ข
๋ฃ์ผ ์ฒ๋ฆฌ ํ
์คํธ ํ์. (์ข ๋ฃ์ผ์ด ์์์ผ ์ด์ ์ผ ๋ ๋ฑ) |
| isRepeatStructureChanged | โญโญโญโญ (์ค์) | ๊ธฐ์กด ์ผ์ ์์ ์ ๋ฐ๋ณต ๊ทธ๋ฃน ์ฌ์์ฑ ์ฌ๋ถ ํ๋จ → ์๋ชป๋๋ฉด ๋ถํ์ํ๊ฒ ๊ทธ๋ฃน ๊นจ์ง๊ฑฐ๋ ์ ๋ฐ์ดํธ ๋๋ฝ๋จ. |
| copyScheduleProperties | โญ (๋ฎ์) | ๋จ์ ์์ฑ ๋ณต์ฌ. ์ผ๋ฐ์ ์ผ๋ก getter/setter ๋จ์๋ ํ ์คํธ ๊ฐ์น ๋ฎ์. ๋์ ์คํ๋ง BeanUtils ๊ฐ์ ํ์ค ์ ํธ ์ฐ๋ฉด ์์ ํ ์คํธ ํ์ ์์. |
๐ ์์ (Reflection ์ฌ์ฉ)
@Test
void testGenerateRepeatedSchedules() throws Exception {
Method method = ScheduleService.class.getDeclaredMethod("generateRepeatedSchedules", ScheduleDTO.class);
method.setAccessible(true);
ScheduleDTO schedule = new ScheduleDTO();
// schedule ์ด๊ธฐํ
List<ScheduleDTO> result = (List<ScheduleDTO>) method.invoke(scheduleService, schedule);
assertNotNull(result);
assertFalse(result.isEmpty());
}
๐ 4. DB ์ฐ๋ ๋จ์ ํ ์คํธ
โ 4-1. ๋ชฉ์
- DB ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊พธ๋ ํจ์ ์ ํ์ฑ ํ์ธ
- ์ค์ ํธ์ถ ๊ฒฝ๋ก ํ ์คํธ → ์ปค๋ฒ๋ฆฌ์ง ํ๋ณด
- ์ด์ DB ๊ฑด๋๋ฆฌ์ง ์๊ณ ์์ ํ๊ฒ ํ
์คํธ
โ 4-2. ๋ฐฉ๋ฒ
- ์ธ๋ฉ๋ชจ๋ฆฌ DB ์ฌ์ฉ (H2 ๋ฑ)
- ํธ๋์ญ์ ๋กค๋ฐฑ
@Transactional
@Test
void testDBUpdate() { ... }
- Mocking (DB ํธ์ถ ์์ฒด๋ฅผ Mock)
๐ 5. ํ ์คํธ ์คํ
โ # ํด๋์ค ์ ์ฒด ์คํ
mvn test
mvn test -Dtest=ScheduleServiceTest (test๊น์ง๋ง ์ณ๋ ๋ํดํธ)
โ # ํน์ ๋ฉ์๋๋ง ์คํ
mvn test -Dtest=ScheduleServiceTest#testUpdateSchedule_RepeatStructureChanged
๐ 6. ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๋?
- ํ
์คํธ ์คํ ์ ์ฝ๋๊ฐ ์ผ๋ง๋ ์คํ๋์๋์ง ๋น์จ๋ก ๋ํ๋ธ ์งํ
โ ์ข ๋ฅ
- ๋ผ์ธ(Line) ์ปค๋ฒ๋ฆฌ์ง: ๋ช ์ค ์คํ๋๋์ง
- ๋ธ๋์น(Branch) ์ปค๋ฒ๋ฆฌ์ง: if/else, switch ๋ถ๊ธฐ ์คํ ์ฌ๋ถ
- ์กฐ๊ฑด(Condition) ์ปค๋ฒ๋ฆฌ์ง: ์กฐ๊ฑด์ true/false ์ผ์ด์ค
๐ก ์ค์ ํ์ ์์๋?
- ์ปค๋ฒ๋ฆฌ์ง ์ธก์ ๋๊ตฌ (JaCoCo, IntelliJ built-in, SonarQube) ๋ก ๋ฆฌํฌํธ๋ฅผ ์์ฑ
- CI/CD ํ์ดํ๋ผ์ธ์ ์ปค๋ฒ๋ฆฌ์ง ๊ฒ์ฆ์ ๊ฑธ์ด๋
- ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง, ์์ธ ์ฒ๋ฆฌ, ๋ถ๊ธฐ๋ฅผ ์ค์ ์ ์ผ๋ก ํ ์คํธ
- ์ปค๋ฒ๋ฆฌ์ง ์์น๋ณด๋ค๋ ํ ์คํธ ์ผ์ด์ค์ ์๋๋ฆฌ์ค ์์ฑ๋๋ฅผ ๋ ์ค์ (ํ ์คํธ๊ฐ ์ง์ง ๋ฒ๊ทธ๋ฅผ ์ก์ ์ ์๋?)
๐ 7. ์ปค๋ฒ๋ฆฌ์ง ์ธก์ ๋ฐฉ๋ฒ - JaCoCo
๐ 7-1. JaCoCo ์์ด์ ํธ๋?
- JVM์์ ์คํ๋๋ ์์ ํ๋ก๊ทธ๋จ(JAR)
- Java ์ ํ๋ฆฌ์ผ์ด์ ์ด ์คํ๋๋ ๋์ ์ด๋ค ์ฝ๋๊ฐ ์คํ๋๋์ง ์ถ์ (track)ํ๋ ์ญํ
- ํ ์คํธ ์คํ ์ ์ฝ๋ ์คํ ์ ๋ณด ๊ธฐ๋ก → jacoco.exec ์์ฑ → ๋ฆฌํฌํธ ๋ณํ ๊ฐ๋ฅ
โ์ด๋ป๊ฒ ๋์ํ ๊น?
1๏ธโฃ Maven์์ jacoco-maven-plugin์ด prepare-agent ๋ชฉํ(goal)๋ฅผ ์คํ
2๏ธโฃ JVM ์ต์ ์ ์๋์ผ๋ก ๋ถ์: -javaagent:"path/to/jacocoagent.jar=destfile=target/jacoco.exec"
3๏ธโฃ JVM์ด ์ฝ๋ ์คํํ ๋ ๋ชจ๋ ํด๋์ค์ ์คํ ์ ๋ณด๋ฅผ ์์ด์ ํธ๊ฐ ๊ฐ๋ก์ฑ์ ๊ธฐ๋ก
4๏ธโฃ ํ ์คํธ ์ข ๋ฃ ํ jacoco.exec ํ์ผ์ ๊ธฐ๋ก ์ ์ฅ
5๏ธโฃ jacoco:report ์คํํ๋ฉด .exec ํ์ผ → HTML/CSV ๋ฑ ๋ฆฌํฌํธ๋ก ๋ณํ
โ 7-2. ์ค์ ๋ฐฉ๋ฒ
๐ Maven ์ค์
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
<executions>
<execution>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals><goal>report</goal></goals>
</execution>
</executions>
</plugin>
โ 7-3. ์คํ ๋ฐฉ๋ฒ
๐น๋จ์ ํ ์คํธ + ์ปค๋ฒ๋ฆฌ์ง ํ์ธ: mvn clean test jacoco:report
๐นํ ์คํธ + ํ์ฅ ๊ฒ์ฆ: mvn clean test verify
- clean → target/ ํด๋ ์ญ์
- test → src/test/java ๋จ์ ํ ์คํธ ์คํ
- jacoco:report → ํ ์คํธ ๊ฒฐ๊ณผ ๊ธฐ๋ฐ์ผ๋ก JaCoCo ์ปค๋ฒ๋ฆฌ์ง ๋ฆฌํฌํธ ์์ฑ
- verify → ํ ์คํธ ํ ์ถ๊ฐ ๊ฒ์ฆ ๋จ๊ณ ์คํ
๐จ 8. ์คํ ์ค๋ฅ - ํ๊ธ ๊ฒฝ๋ก, jdk๋ฒ์ (CMD์์ ์คํ)
โ 8-1. ๊ฒฝ๋ก ๋ฌธ์
IDE๋ด์์ ์คํ ์ ํ๊ธ ๊ฒฝ๋ก ์ธ์ ๋ชป ํจ.
๐น ์์ธ
IDE ๋จ์ ์คํ → Maven ๊ฐ์ X → JaCoCo ์ต์ ์ ์ฉ ์ ๋จ → .exec ํ์ผ ์์ฑ ์คํจ
๐น ํด๊ฒฐ
- Maven Surefire ์์๊ฒฝ๋ก ์ง์ : argLine + JaCoCo destFile ์ง์
- ์์ ํด๋ ์๋ฌธ ๊ฒฝ๋ก ์์ฑ ๋ฐ Cmd์์ ์คํ
1๏ธโฃ pom.xml ์ค์
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<argLine>-javaagent:C:/Temp/.m2/org/jacoco/org.jacoco.agent/0.8.8/org.jacoco.agent-0.8.8-runtime.jar=destfile=C:/Temp/TestMvn/jacoco.exec</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
<configuration>
<destFile>C:/Temp/jacoco.exec</destFile>
<dataFile>C:/Temp/jacoco.exec</dataFile>
<outputDirectory>${project.build.directory}/jacoco-report</outputDirectory>
</configuration>
</plugin>
2๏ธโฃ ์์ ๋๋ ํ ๋ฆฌ ์ธํ ๋ฐ cmd ๋ช ๋ น
- ํ๋ก์ ํธ ๋ฃจํธ ๊ฒฝ๋ก์์ ์คํ
set MAVEN_OPTS=-Dmaven.repo.local=C:\Temp\.m2
set JAVA_TOOL_OPTIONS=-Duser.home=C:\Temp\javahome
mvn clean test jacoco:report
โ๏ธ cmd ๋ซ์ผ๋ฉด ์ฌ๋ผ์ง๋ ์์ ์ค์
โ ๋จ์ : ๊ธฐ์กด .m2 ์บ์๋ ๋ชป ์ฐ๊ณ , ์์ ํด๋์ ์๋ก ๋ค์ด๋ก๋๋จ (์ปค์คํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ?๋ ์ง์ ์๋ก ์์ฑํ .m2 ๋ฐ์ ๋ณต์ฌํด์ผํจ. ํ์ผ๋ช ๋ ๋ฒ์ ๋ถ์ฌ์!!
- ์ปค์คํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์ : C:\Temp\.m2\local\lib\MessageCalendarAseUtil\1.0\MessageCalendarAseUtil-1.0.jar
<!-- MessageCalendarAseUtil -->
<dependency>
<groupId>local.lib</groupId>
<artifactId>MessageCalendarAseUtil</artifactId>
<version>1.0</version>
</dependency>
โ 7-2. JDK ๋ฒ์ ๋ฌธ์
PC์ jdk8, jdk17 ์๊ณ ํ๋ก์ ํธ๋ง๋ค ๋ค๋ฅธ ๋ฒ์ ์ด์ฉ. ๊ธฐ๋ณธ 8.
ํด๋น ํ๋ก์ ํธ๋ IDE๋ด์์ 17์ฌ์ฉํ๋๋ก ์ค์ ํด์ ์ด์ฉํ์์.
๐น์์ธ
- ๋ฉ์ด๋ธ JDK ๋ฒ์
- mvn -v ์ ๋ ฅ ์ ์ถ๋ ฅ
Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937)
Maven home: C:\Program Files\Apache\Maven\apache-maven-3.9.9
Java version: 1.8.0_202, vendor: Oracle Corporation, runtime: C:\Program Files\Java\jdk1.8.0_202\jre
Default locale: ko_KR, platform encoding: MS949
OS name: "windows 10", version: "10.0", arch: "amd64", family: "windows"
๐นํด๊ฒฐ
set JAVA_HOME=C:\Program Files\Java\jdk-17
set PATH=%JAVA_HOME%\bin;%PATH%
๐น์์ ํ
- mvn -v ์ ๋ ฅ ์ ์ถ๋ ฅ
Picked up JAVA_TOOL_OPTIONS: -Duser.home=C:\Temp\TestMvn\javahome
Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937)
Maven home: C:\Program Files\Apache\Maven\apache-maven-3.9.9
Java version: 17.0.13, vendor: Oracle Corporation, runtime: C:\Program Files\Java\jdk-17
Default locale: ko_KR, platform encoding: MS949
OS name: "windows 11", version: "10.0", arch: "amd64", family: "windows"
๐8. ํ ์คํธ ๊ฒฐ๊ณผ
โ 8-1. ํ ์คํธ ๊ฒฐ๊ณผ ํ์ธ
๐นํ์ผ๋ณ ํ ์คํธ ๊ฒฐ๊ณผ + ์ ์ฒด ๊ฒฐ๊ณผ
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 15.97 s -- in kr.co.example.calender.controller.ScheduleRestControllerTest
[INFO] Running kr.co. example.calender.service.ScheduleServicePrivateTest
[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.395 s -- in kr.co.example.calender.service.ScheduleServicePrivateTest
[INFO] Running kr.co.example.calender.service.ScheduleServiceTest
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.052 s -- in kr.co.example.calender.service.ScheduleServiceTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 22, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- jacoco:0.8.8:report (default-cli) @ calendar ---
[INFO] Loading execution data file C:\Temp\TestMvn\jacoco.exec
[INFO] Analyzed bundle 'calendar' with 48 classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:14 min
[INFO] Finished at: 2025-08-18T11:27:50+09:00
[INFO] ------------------------------------------------------------------------

โ 8-2. ์ปค๋ฒ๋ฆฌ์ง ๋ฆฌํฌํธ ํ์ธ
๐น ์์ฑ์์น: Calendar\target\jacoco-report\index.html
๐น ๋ธ๋ผ์ฐ์ ์์ ์คํ
๋๋ ํ ๋ฆฌ -> ํด๋์ค -> ๋ฉ์๋ -> ์ฝ๋ ๋ผ์ธ ์คํ ์ ๋ฌด ํ์ธ ๊ฐ๋ฅ

๐ ํ๊ธฐ
๐น ์ฃผ์ ๋ก์ง ํ ์คํธ
์ฃผ์ ๋ก์ง์ ScheduleService์ด์ด์, ์ด ์๋น์ค ์์ฃผ๋ก ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ค.
๐น ์กฐ๊ฑด๋ณ ํ ์คํธ ์์ฑ๊ณผ ์ค๋ฅ ๊ฒฝํ
์์์ JUnit์ ๋ํด ๊ณต๋ถํ๋ฉฐ ๊ธฐ๋ณธ ๋ฐ์ดํฐ๋ง์ผ๋ก ํ ์คํธํ์ ๋ ์๋น์ค ์ปค๋ฒ๋ฆฌ์ง๋ ์ฝ 5%์๋๋ฐ, ์ถ๊ฐ๋ก ์กฐ๊ฑด๋ฌธ ์ผ์ด์ค๋ณ๋ก ํ ์คํธ ํจ์๋ฅผ ์์ฑํด๋ดค๋ค.
์๋ฅผ ๋ค์ด, "์ผ์ ์์ ํจ์ - ๋ฐ๋ณต ์ผ์ ์์ " ์ผ์ด์ค๋ฅผ ํ
์คํธ๋ฅผ ์๋์ ๊ฐ์ด ์์ฑ.
@Test
@DisplayName("์ผ์ ์์ ์ฑ๊ณต ํ
์คํธ - ๋ฐ๋ณต ๊ตฌ์กฐ ๋ณ๊ฒฝ ์ ์ญ์ ํ ์ฌ์์ฑ")
void testUpdateSchedule_RepeatStructureChanged() {
// Given
ScheduleDTO updateSchedule = new ScheduleDTO();
BeanUtils.copyProperties(testSchedule, updateSchedule); // Spring BeanUtils ์ฌ์ฉ
updateSchedule.setSchdTitle("์์ ๋ ์ผ์ ");
updateSchedule.setModifierId("USER001");
updateSchedule.setUpdateMode("CURRENT");
// ๋ฐ๋ณต ๊ตฌ์กฐ ๋ณ๊ฒฝ
updateSchedule.setReptTypeCd("WEEKLY");
updateSchedule.setBeginDate("2024-02-15");
updateSchedule.setReptFinDate("2024-03-10");
when(scheduleMapper.selectSchedule("SCHD001"))
.thenReturn(testSchedule);
when(scheduleMapper.selectScheduleWithSharedUsers("SCHD001"))
.thenReturn(testSchedule);
// when(scheduleMapper.deleteScheduleSharedUsers("SCHD001"))
// .thenReturn(1);
when(scheduleMapper.deleteSchedule("SCHD001"))
.thenReturn(1);
when(userService.svcSelectCompnDept("USER001"))
.thenReturn(testUser);
when(scheduleMapper.insertSchedule(any(ScheduleDTO.class)))
.thenReturn(1);
// When
// try (MockedStatic<SecurityUtil> mockedSecurityUtil = mockStatic(SecurityUtil.class)) {
// mockedSecurityUtil.when(SecurityUtil::getAuthUserId).thenReturn("USER001");
int result = scheduleService.svcUpdateSchedule(updateSchedule);
// Then
assertAll(
() -> assertEquals(1, result),
() -> verify(scheduleMapper).selectSchedule("SCHD001"),
() -> verify(scheduleMapper).selectScheduleWithSharedUsers("SCHD001"),
// () -> verify(scheduleMapper).deleteScheduleSharedUsers("SCHD001"),
() -> verify(scheduleMapper).deleteSchedule("SCHD001"),
() -> verify(userService).svcSelectCompnDept("USER001"),
() -> verify(scheduleMapper).insertSchedule(any(ScheduleDTO.class))
);
// }
}
ํ๋ฉด์ ๋ก์ง ๊ฒ์ฆ์ด ๊ฐ๋ฅํ๋ค.
deleteScheduleSharedUsers์ด๊ฑฐ ๋ฐ๋ก ์คํ๋๋ ์ค ์์๋๋ฐ ์๋ ์ค๋ฅ๋ ์ ๋ณด๋ selectScheduleWithSharedUsers๊ฒฐ๊ณผ ์์๋๋ง ์คํ.
*Wanted but not invoked: ํ ์คํธ์์ verifyํ ๋ฉ์๋๋ค์ด ํธ์ถ๋์ง ์์.
org.mockito.exceptions.verification.WantedButNotInvoked: Wanted but not invoked: scheduleMapper.deleteScheduleSharedUsers( "SCHD001" );
์ด์ฒ๋ผ Mockito์ verify๋ฅผ ํตํด ์๋น์ค ๋ฉ์๋ ๋ด ํธ์ถ ์์์ ์กฐ๊ฑด์ ํ์ธ๊ฐ๋ฅ ํ๊ณ , ํ ์คํธ ์ฝ๋๋ฅผ ๋จ์ ๊ฒ์ฆ์ฉ๋ฟ ์๋๋ผ ๋ฉ์๋ ์คํ ํ๋ฆ ํ์ธ์ฉ์ผ๋ก๋ ํ์ฉํ ์ ์์ ๊ฒ ๊ฐ๋ค.
๐น ์ปค๋ฒ๋ฆฌ์ง์ private ๋ฉ์๋ ํ
์คํธ
์กฐ๊ฑด๋ณ ํ
์คํธ๋ฅผ ์์ฑํ์ ๋ ์๋น์ค ์ปค๋ฒ๋ฆฌ์ง๋ 18%๊น์ง ์ฌ๋ผ๊ฐ๊ณ , ScheduleService ๋ด ํต์ฌ ๋ก์ง์ 50% ์ด์ ์ปค๋ฒํ ์ ์์๋ค. ๋ํ private ๋ฉ์๋๋ public ๋ฉ์๋๋ฅผ ํตํด ๊ฐ์ ์ ์ผ๋ก ํ
์คํธ๊ฐ ๊ฐ๋ฅํ๋ค.
๐น ๋๋จธ์ง 50%์๋ ์ฌ์ฉ์ ๋ฉ์ผ/์๋ฆผ(์ฐ๋์๋น์ค) ๋ฑ์ด ํฌํจ๋์ด์ ์ฌ๊ธฐ๊น์ง๋ง ํ์ฉํด๋ด.


'Backend > JAVA' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| ๋๋ DB INSERT ์ต์ ํ (0) | 2025.11.29 |
|---|---|
| ํ์ผ ์ ๋ก๋, ๋ค์ด๋ก๋ (MultipartFile) (0) | 2025.11.08 |
| Stream API๋? (for-loop์ ๋น๊ต) (0) | 2025.08.09 |
| static ํค์๋ (0) | 2025.02.26 |
| ์๋ฐ(Spring) ๊ธฐ๋ณธ ์์ธ ์ฒ๋ฆฌ (0) | 2025.02.23 |