๐ ๋ฐฐ๊ฒฝ
API๋ฅผ ๊ตฌํํ ๋ ์๊ตฌ์ฌํญ์ ์กฐ๊ฑด์ด๋ ๊ธฐ๋ฅ์ ์์ฑ ์๊ฐ, ์์ ์๊ฐ์ด ํฌํจ๋์ด ์๋ ๊ฒฝ์ฐ, ์ฌ๋ฐ๋ฅธ ์๊ฐ ๊ฐ์ ๋ฐํํ๋์ง ํ์ธํด์ผ ํ ํ์๊ฐ ์๋ค.
๋ณดํต JPA๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ์์ฑ์/์์ฑ ์ผ์/์์ ์/์์ ์ผ์๋ฅผ ๊ฐ๋จํ๊ฒ ์ถ๊ฐํ๊ธฐ ์ํด BaseEntity, Audit ๊ธฐ๋ฅ์ ๋ง์ด ์ฌ์ฉํ ๊ฒ์ด๋ค. ํ์๊ฐ ์งํ ์ค์ธ ํ๋ก์ ํธ์๋ ๋งค๋ฒ Audit๋ฅผ ํ์ฉํ๊ณ ์๋ค.
๊ทธ๋ฐ๋ฐ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ค ๋ณด๋ฉด ์กฐํ ์กฐ๊ฑด์ด๋ ๊ฒฐ๊ณผ์ ์์ฑ/์์ ์๊ฐ ๊ฐ์ ์ฌ๋ฐ๋ฅด๊ฒ ์ ์ฉํ๊ณ ๋ฐํํ๋์ง ๊ถ๊ธํ ๋๊ฐ ์๋ค. ๊ทธ๋ด ๋ ์ด๋ป๊ฒ ํ ์คํธ๋ฅผ ํด์ผ ํ ์ง ๊ณ ๋ฏผํ๋ค๊ฐ ํด๋น ํฌ์คํ ์ ์์ฑํ๊ฒ ๋์๋ค.
๋ฐฉ๋ฒ
ํ์๊ฐ ์ฐพ์๋ณธ ๋ฐฉ๋ฒ ์ค ์ฌ์ฉํ๊ธฐ ๊ฐ์ฅ ๊ด์ฐฎ์ ๋ฐฉ๋ฒ์ 2๊ฐ์ง ๋ฐฉ๋ฒ์ด์๋ค.
์ฒซ ๋ฒ์งธ๋ DateTimeProvider Mocking์ ํตํด์ ์๊ฐ ๊ฐ์ Mocking ํ๋ ๋ฐฉ๋ฒ์ด๋ค.
๋ ๋ฒ์งธ๋ JdbcTemplate์ ์ด์ฉํด์ ์๊ฐ ๊ฐ์ ์กฐ์ํ๋ ๋ฐฉ๋ฒ์ด๋ค.
1. Mocking
@SpringBootTest
@Transactional
public class AuditTest {
@Autowired
ShibaHolicRepository shibaHolicRepository;
@MockBean
private DateTimeProvider dateTimeProvider;
@SpyBean
private AuditingHandler handler;
@BeforeEach
void setUp() throws Exception {
MockitoAnnotations.openMocks(this);
handler.setDateTimeProvider(dateTimeProvider);
}
@Test
void auditingTest() {
// given
LocalDateTime testTime = LocalDateTime.of(2023,10,30,0,0,0);
when(dateTimeProvider.getNow()).thenReturn(Optional.of(testTime));
ShibaHolic shibaHolic1 = ShibaHolic.builder()
.name("์๋ฐํ๋ฆญ1")
.age(10L)
.build();
shibaHolicRepository.save(shibaHolic1);
ShibaHolic shibaHolic2 = ShibaHolic.builder()
.name("์๋ฐํ๋ฆญ2")
.age(20L)
.build();
shibaHolicRepository.save(shibaHolic2);
// when
List<ShibaHolic> result = shibaHolicRepository.findAll();
// then
assertThat(result).hasSize(2)
.extracting("createdAt")
.containsOnly(testTime);
}
}
ํด๋น ๋ฐฉ์์ DateTimeProvider๋ฅผ Mocking ํ๊ณ AuditingHandler์ ๋ฑ๋กํด์ ์ฌ์ฉํ๋ค.
@Test
void auditingTest() {
// given
LocalDateTime testTime = LocalDateTime.of(2023,10,30,0,0,0);
when(dateTimeProvider.getNow()).thenReturn(Optional.of(testTime));
ShibaHolic shibaHolic1 = ShibaHolic.builder()
.name("์๋ฐํ๋ฆญ1")
.age(10L)
.build();
shibaHolicRepository.save(shibaHolic1);
LocalDateTime testTime2 = LocalDateTime.of(2023,10,31,0,0,0);
when(dateTimeProvider.getNow()).thenReturn(Optional.of(testTime2));
ShibaHolic shibaHolic2 = ShibaHolic.builder()
.name("์๋ฐํ๋ฆญ2")
.age(20L)
.build();
shibaHolicRepository.save(shibaHolic2);
// when
List<ShibaHolic> result = shibaHolicRepository.findAll();
// then
assertThat(result).hasSize(2)
.extracting("createdAt")
.contains(testTime, testTime2);
}
๋ง์ฝ ๋ฑ๋กํ๋ ์ํฐํฐ์ ๋ฐ๋ผ์ ์๊ฐ ์ฐจ์ด๋ฅผ ์ฃผ๊ณ ์ถ๋ค๋ฉด, Repository save()๋ฅผ ํธ์ถํ๊ธฐ ์ ์ when() ์ ์ ํ ๋ฒ ๋ ์ ์ธํด์ ๋ค๋ฅธ ์๊ฐ์ ์ถ๊ฐํ๋ฉด ๋๋ค.
2. JdbcTemplate
@SpringBootTest
@Transactional
public class AuditTest2 {
@Autowired
ShibaHolicRepository shibaHolicRepository;
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
EntityManager entityManager;
@Test
void auditingTest() {
// given
ShibaHolic shibaHolic1 = ShibaHolic.builder()
.name("์๋ฐํ๋ฆญ1")
.age(10L)
.build();
ShibaHolic savedShibaHolic1 = shibaHolicRepository.save(shibaHolic1);
// 10์ 29์ผ
String sql = "update tb_shiba_holic set created_at = DATE_SUB('2023-10-30 00:00:00', INTERVAL 1 DAY) where id = " + savedShibaHolic1.getId() + " ";
jdbcTemplate.execute(sql);
ShibaHolic shibaHolic2 = ShibaHolic.builder()
.name("์๋ฐํ๋ฆญ2")
.age(20L)
.build();
ShibaHolic savedShibaHolic2 = shibaHolicRepository.save(shibaHolic2);
// 10์ 31์ผ
String sql2 = "update tb_shiba_holic set created_at = DATE_ADD('2023-10-30 00:00:00', INTERVAL 1 DAY) where id = " + savedShibaHolic2.getId() + " ";
jdbcTemplate.execute(sql2);
entityManager.flush();
entityManager.clear();
// when
List<ShibaHolic> result = shibaHolicRepository.findAll();
// then
assertThat(result).hasSize(2)
.extracting("createdAt")
.contains(LocalDateTime.of(2023,10,29,0,0,0),
LocalDateTime.of(2023,10,31,0,0,0));
}
}
ํด๋น ๋ฐฉ์์ jdbcTemplate์ ์ฌ์ฉํด์ ์ง์ SQL ๋ฌธ์ ์์ฑํด์ ์๊ฐ์ ๋ณ๊ฒฝํ๋ ๋ฐฉ๋ฒ์ด๋ค.
๊ทธ๋ฐ๋ฐ ํด๋น ๋ฐฉ๋ฒ์ ๊ทธ๋ฅ ์ํฐํฐ์์ ์๊ฐ์ ์ ๋ฐ์ดํธํ๋ฉด ๋์ง ์์๊น?๋ผ๋ ๊ณ ๋ฏผ์ด ๋ค๊ฒ ํ๋ค.
๋ณดํต BaseEntity๋ updatable=false ์กฐ๊ฑด์ ์ถ๊ฐํ๋ ๊ฒ ์ผ๋ฐ์ ์ด๊ธฐ ๋๋ฌธ์ ์ ๋ฐ์ดํธ๋ฅผ ํ๊ธฐ ์ํด์๋ ํด๋น ์ต์ ์ ํ์ด์ผ ํ๋ค. ๊ณผ์ฐ ํ ์คํธ๋ฅผ ์ํด์ ์ด๋ฌํ ์ ์ฝ ์กฐ๊ฑด ํด์ ๊น์ง ํด์ผ ํ ๊น? ํ๋จํ๋ ๊ฑด ๊ฐ๋ฐ์์ ๋ชซ์ด๋ค.
์ฅ/๋จ์
ํ์๋ ๊ฐ ๋ฐฉ์์ ์ฅ/๋จ์ ์ด ์๋ค๊ณ ์๊ฐํ๋ค.
Mocking ๋ฐฉ์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํต์ ๊ณผ์ ์ด ์์ผ๋ฏ๋ก ์๋๊ฐ ๋น ๋ฅด๊ณ , SQL๋ฌธ ์์ฑ ์์ด ๋ด๊ฐ ํ์ํ ์๊ฐ์ผ๋ก ์ฝ๊ฒ ์กฐ์ํ ์ ์์ด์ ํธ๋ฆฌํ๋ค๋ ์ฅ์ ์ด ์๋ค.
ํ์ง๋ง, ๋ง์ฝ ์์(๋ถ๋ชจ) ํ ์คํธ ํด๋์ค์์ BaseEntity๋ฅผ ์ฌ์ฉํ๋ ๋ค๋ฅธ ๊ฒฝ์ฐ๊ฐ ์๋ค๋ฉด ์ฌ์ฉ์ด ๋ถ๊ฐ๋ฅํ๋ค๋ ๋จ์ ์ด ์๋ค.
(๋ณ๋๋ก ๋ถ๋ฆฌํ๋ค๋ฉด ์คํ๋ง ์ปจํ ์คํธ๊ฐ 1๊ฐ ๋ ๋์์ง๊ธฐ ๋๋ฌธ์, ์ ์ฒด ํ ์คํธ์์๋ ์๊ฐ์ ์ํด๋ฅผ ๋ณผ ์ ์๋ค.)
JdbcTemplate ๋ฐฉ์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฐ์ ์ง์ ์กฐ์ํ๊ธฐ ๋๋ฌธ์ ๋ณ๋ Mocking ์์ ์ด ํ์ ์๊ณ , ์์(๋ถ๋ชจ) ํ ์คํธ ์ฝ๋๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์๋ค๋ ์ฅ์ ์ด ์๋ค.
ํ์ง๋ง, ํ ์คํธ ์ฝ๋์์ ์ฌ์ฉํ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ๋ซํผ์ ๋ง๊ฒ Update SQL๋ฌธ์ ์์ฑํด์ผ ํ๊ณ , ๋ง์ฝ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๋ณ๊ฒฝํ๋ค๋ฉด ํ ์คํธ ์ฝ๋๊ฐ ์ฝ๊ฒ ๊นจ์ง ์ ์๋ค๋ ๋จ์ ์ด ์๋ค.
๊ฒฐ๋ก
์ด๋ค ๊ฒฝ์ฐ์ ์ด๋ป๊ฒ ์ฐ๋ฉด ์ข์๊น?
ํ์๋ Auditing์ ๊ฒ์ฆํ๋ ๊ณผ์ ์ด ํ ์คํธ ์ฝ๋์ ๋ง์ด ์ฌ์ฉ๋๊ฑฐ๋, ๋จ์ ํ ์คํธ๋ก ์ฌ์ฉ๋๊ฑฐ๋, ์์ ํ ์คํธ ์ฝ๋์ ๋ถ๋ฆฌ๋์ด ์๋ ๋ฐฉ์์ด๋ผ๋ฉด Mocking ๋ฐฉ์ ์ฌ์ฉ์ ์ถ์ฒํ๊ณ ์ถ๋ค.
ํ์ง๋ง ๋ช ๋ฒ ๊ฒ์ฆ์ด ํ์ํ์ง ์์ ์ผ๋ถ ์ผ์ด์ค์๋ง ์ฌ์ฉํ๋ค๋ฉด JdbcTemplate ์ฌ์ฉ์ ์ถ์ฒํ๊ณ ์ถ๋ค.
๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ํ ์คํธ ๊ฐ๋ฐ ํ๊ฒฝ์ ์๋ง์ ๋ฐฉ๋ฒ์ ์ ํํด์ ์ฌ์ฉํด ๋ณด๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค.
REFERENCE
https://mariadb.com/kb/en/date_add/
https://mariadb.com/kb/en/date_sub/
https://mkyong.com/spring-boot/mocking-spring-data-datetimeprovider/
https://stackoverflow.com/questions/42374387/how-to-set-createddate-in-the-past-for-testing
๋๊ธ