์ด์ ์ ํ๋ ํ๋ก์ ํธ๋ค์ ๋ํด์ ํ ์คํธ์ฝ๋๋ ์์ฑํ์ง ์์๋ค. ์์ฑํ๋ค ํด๋ ์ ํ ์คํธ์ฝ๋๋ฅผ ์์ฑํ๋์ง๋ ์ ๋ชจ๋ฅด๊ณ ๋์ถฉ ๋์ ์ฌ๋ณธ๊ฒ ๋ค์๋ค.
tickerBell ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์ ํตํฉํ ์คํธ, ๋จ์ํ ์คํธ๋ฅผ ์งํํ๋ฉฐ ์์ฑ๋ ๋์ ํ๋ก์ ํธ๋ฅผ ์์ฑํด๋ณด๋ ค ํ๋ค.
์ฐ์ ๋จ์ํ ์คํธ์ ๋ง์ด ์ฌ์ฉํ๋ Mock, Mockito์ ๋ํด ์๊ฐํ๊ณ ์ ํ๋ค.
Mock์ด๋?
Mock์ ํ๊ธ๋ก "๋ชจ์, ๊ฐ์ง์"๋ผ๋ ๋ป์ผ๋ก ํ ์คํธํ ๋ ํ์ํ ์ค์ ๊ฐ์ฒด์ ๋์ผํ ๋ชจ์ ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด ํ ์คํธ์ ํจ์ฉ์ฑ์ ๋์ด๊ธฐ ์ํด ์ฌ์ฉํ๋ค.
์์ ๋ฅผ ๋ณด๋ฉด์ ์ Mock์ ์ฌ์ฉํ๋์ง ์์๋ณด์
@Service
public class StudyService {
public Student getStudent() {
// DB์์ study ํ
์ด๋ธ ์กฐํ (๋ถํ๊ฐ ๋ง์ด ๊ฑธ๋ฆฌ๋ ์์
)
}
}
์ ์ฝ๋๋ฅผ ํ ์คํธํ๋ค๋ฉด ๋งค๋ฒ ํ ์คํธํ ๋๋ง๋ค DB๋ฅผ ์ฝ์ด์ค๊ฒ๋๋ค. ๋ง์ฝ ์ ๋ก์ง์ด ๋ถํ๊ฐ ๋ง์ด ๊ฑธ๋ฆฌ๊ณ ์ํ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฐ๋ค๋ฉด ํ ์คํธ ์ฝ๋๋ฅผ ์คํํ๋๋ฐ ์๋นํ ์๊ฐ์ด ์์๋๋ค.
๊ทธ๋์ Student๋ฅผ DB์์ ์ฝ์ด์ค์ง ์๊ณ ์ด๋ฅผ ๊ฐ์ง ๊ฐ์ฒด ์ฆ, mock์ผ๋ก ๋ง๋ค์ด์ DB์ ์๋ ํ ์ด๋ธ ์ ๊ทผ์ ์ต์ํํ ์ ์๋ค.
mock ๊ฐ์ฒด๋ฅผ ๋ง๋ค๋ฉด ํ ์คํธ ์๊ฐ์ ์ค์ด๋ฉด์ ๋ถํ์ํ ๋ฆฌ์์ค ์๋น๋ฅผ ๋ง์ ์ ์๋ค.
๋ํ ์ธํฐํ์ด์ค๋ง ๋์จ ์์ ์ ๋ํด์๋ mock ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ๊ตฌํ ์ ์ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๋ค.
Mockito๋?
mock์ ์ฝ๊ฒ ๋ง๋ค๊ณ mock์ ํ๋์ ์ ํ๋ stubbing, ์ ์์ ์ผ๋ก ์๋ํ๋์ง์ ๋ํ verify ๋ฑ ๋ค์ํ ๊ธฐ๋ฅ์ ์ ๊ณตํด์ฃผ๋ ํ๋ ์์ํฌ์ด๋ค.
@Mock
@Mock์ผ๋ก ๋ง๋ mock ๊ฐ์ฒด๋ ๊ฐ์ง ๊ฐ์ฒด์ด๋ฉฐ ๊ทธ ์์ ๋ฉ์๋ ํธ์ถํด์ ์ฌ์ฉํ๋ ค๋ฉด ๋ฐ๋์ ์คํฐ๋น(stubbing)์ ํด์ผํ๋ค.
@Spy
@Spy๋ก ๋ง๋ mock ๊ฐ์ฒด๋ ์ง์ง ๊ฐ์ฒด์ด๋ฉฐ ๋ฉ์๋ ์คํ ์ ์คํฐ๋น์ ํ์ง ์์ผ๋ฉด ๊ธฐ์กด ๊ฐ์ฒด์ ๋ก์ง์ ์คํํ ๊ฐ์, ์คํฐ๋น์ ํ ๊ฒฝ์ฐ์ ์คํฐ๋น ๊ฐ์ ๋ฆฌํดํ๋ค.
@InjectMock
@InjectMock์ DI๋ฅผ @Mock์ด๋ @Spy๋ก ์์ฑ๋ mock ๊ฐ์ฒด๋ฅผ ์๋์ผ๋ก ์ฃผ์ ํด์ฃผ๋ ์ด๋ ธํ ์ด์ ์ด๋ค.
OngoingStubbing ๋ฉ์๋
when์ ๋ฃ์ ๋ฉ์๋์ ๋ฆฌํด ๊ฐ์ ์ ์ํด์ฃผ๋ ๋ฉ์๋์ด๋ค.
when({์คํฐ๋นํ ๋ฉ์๋}).{OngoingStubbing ๋ฉ์๋};
๋ฉ์๋๋ช | ์ค๋ช |
thenReturn | ์คํฐ๋นํ ๋ฉ์๋ ํธ์ถ ํ ์ด๋ค ๊ฐ์ฒด๋ฅผ ๋ฆฌํดํ ๊ฑด์ง ์ ์ |
thenThrow | ์คํฐ๋นํ ๋ฉ์๋ ํธ์ถ ํ ์ด๋ค Exception์ Throwํ ๊ฑด์ง ์ ์ |
thenCallRealMethod | ์ค์ ๋ฉ์๋ ํธ์ถ |
Stubber ๋ฉ์๋
Stubber ๋ฉ์๋๋ OngoingStubbing๊ณผ ๋ค๋ฅด๊ฒ when์ ์คํฐ๋นํ ํด๋์ค๋ฅผ ๋ฃ๊ณ ๊ทธ ํ์ ๋ฉ์๋๋ฅผ ํธ์ถํ๋ค. ๊ทธ ์ด์ ๋ ์คํฐ๋น์ด ๋ฐ๋์ ์คํ๋์ผ ํ๋ ๊ฒฝ์ฐ ์ฌ์ฉํ๋ ๋ฉ์๋์ด๊ธฐ ๋๋ฌธ์ด๋ค.
{Stubber ๋ฉ์๋}.when({์คํฐ๋นํ ํด๋์ค}).{์คํฐ๋นํ ๋ฉ์๋}
๋ฉ์๋๋ช | ์ค๋ช |
doReturn | ์คํฐ๋น ๋ฉ์๋ ํธ์ถ ํ ์ด๋ค ํ๋์ ํ ๊ฑด์ง ์ ์ |
doThrow | ์คํฐ๋น ๋ฉ์๋ ํธ์ถ ํ ์ด๋ค Exception์ throwํ ๊ฑด์ง ์ ์ |
doAnswer | ์คํฐ๋น ๋ฉ์๋ ํธ์ถ ํ ์์ ์ ํ ์ง customํ๊ฒ ์ ์ |
doNothing | ์คํฐ๋น ๋ฉ์๋ ํธ์ถ ํ ์ด๋ค ํ๋๋ ํ์ง ์๊ฒ ์ ์ |
doCallRealMethod | ์ค์ ๋ฉ์๋ ํธ์ถ |
Verify
verify ๋ฉ์๋๋ฅผ ์ด์ฉํด์ ์คํฐ๋นํ ๋ฉ์๋๊ฐ ์คํ๋๋์ง, n๋ฒ ์คํ๋๋์ง, ์คํ์ด ์ด๊ณผ๋์ง ์์๋์ง ๋ฑ ๋ค์ํ๊ฒ ๊ฒ์ฆํด๋ณผ ์ ์๋ค.
verify(T mock, VerificationMode mode).{๋ฉ์๋}
๋ฉ์๋๋ช | ์ค๋ช (ํ ์คํธ ๋ด์์~) |
times(n) | ๋ช ๋ฒ์ด ํธ์ถ๋๋์ง ๊ฒ์ฆ |
never | ํ ๋ฒ๋ ํธ์ถ๋์ง ์์๋์ง ๊ฒ์ฆ |
atLeastOne | ์ต์ ํ ๋ฒ์ ํธ์ถ๋๋์ง ๊ฒ์ฆ |
atLeast(n) | ์ต์ n ๋ฒ์ด ํธ์ถ๋๋์ง ๊ฒ์ฆ |
atMostOnce | ์ต๋ ํ ๋ฒ์ด ํธ์ถ๋๋์ง ๊ฒ์ฆ |
atMost(n) | ์ต๋ n ๋ฒ์ด ํธ์ถ๋๋์ง ๊ฒ์ฆ |
calls(n) | n๋ฒ์ด ํธ์ถ๋๋์ง ๊ฒ์ฆ (InOrder๋ ๊ฐ์ด ์ฌ์ฉํด์ผ ํจ) |
only | ํด๋น ๊ฒ์ฆ ๋ฉ์๋๋ง ์คํ๋๋์ง ๊ฒ์ฆ |
timeout(long mills) | n ms ์ด์ ๊ฑธ๋ฆฌ๋ฉด Fail ๊ทธ๋ฆฌ๊ณ ๋ฐ๋ก ๊ฒ์ฆ ์ข ๋ฃ |
after(long mills) | n ms ์ด์ ๊ฑธ๋ฆฌ๋์ง ํ์ธ timeout๊ณผ ๋ค๋ฅด๊ฒ ์๊ฐ์ด ์ง๋๋ ๋ฐ๋ก ๊ฒ์ฆ ์ข ๋ฃ๊ฐ ๋์ง ์๋๋ค. |
description | ์คํจํ ๊ฒฝ์ฐ ๋์ฌ ๋ฌธ๊ตฌ |
์์ธ ํ ์คํธ
์์ธ ๋ฐ์์ ๋ํ ํ ์คํธ ํ๋ ๋ฐฉ๋ฒ์ ์ฌ๋ฌ๊ฐ์ง๊ฐ ์์๋ค. ๋ง์ด ์ฌ์ฉํ๋ 4๊ฐ์ง๊ฐ ์์๋๋ฐ ๊ทธ ์ค ๊ฐ์ฅ ๊ฐ๋ ์ฑ์ด ๋๊ณ ๋ง์ด ์ฌ์ฉํ๋ assertj์ assertThatTrownBy๋ฅผ ์ฌ์ฉํ๊ธฐ๋ก ํ๋ค. ์์ธ ํ ์คํธ 4๊ฐ์ง๋ ์๋์ ๊ฐ๋ค.
1. assertThrows
2. assertj์ assertThatThrownBy
3. assertEquals (try ~ catch)
4. assertThrows ๋ฐํ๊ฐ ์ฌ์ฉ
- ์ฌ์ฉ ์์
assertThatThrownBy(() -> ticketingService.saveTicketing(memberId, request))
.isInstanceOf(CustomException.class)
.hasMessageContaining(ErrorCode.SEAT_INFO_NOT_VALID_FORMAT.getErrorMessage());
ํ ์คํธ ์ฝ๋์์ ์ํฐํฐ @ID ๊ฐ Null ์ด์
์ด๋ฒ ํ๋ก์ ํธ์์ ์ํฐํฐ์ setter๋ฅผ ์ฌ์ฉํ์ง ์๊ธฐ๋ก ํ๋ค. ๋ค์ํ ์ด์ ๋ค์ด ์์ง๋ง ํด๋น ๊ฒ์๊ธ์ ํ ์คํธ ๊ด๋ จ ๊ฒ์๊ธ์ด๊ธฐ ๋๋ฌธ์ ๋์ด๊ฐ๊ฒ ๋ค. ํ์ง๋ง ์ด๋ ๊ฒ ๋ ๊ฒฝ์ฐ ํ ์คํธ์ฝ๋์์ ์ํฐํฐ๋ฅผ ์์ฑํ ๋ Id๊ฐ์ ์ง์ ํด์ฃผ์ง ๋ชป ํด nullPointException์ด ํฐ์ก๋ค.
Member member = memberRepository.findByEmail(email);
teamRepository.findTeamByMember(member.getId())
์์ ๊ฐ์ ๋ก์ง์ service๋ฅผ ํ ์คํธ ํ๋ค๊ณ ๊ฐ์ ํ์.
memberRepository์ ๋ฐํ๊ฐ์ ๋ฐ๋ก ๋ง๋ member๋ก ์คํฐ๋น ํด์ค ๊ฒ์ด๋ค. ํ์ง๋ง member์ Id๊ฐ์ด null ์ด๋ฏ๋ก ์๋ฌ๊ฐ ๋๊ฒ ๋๋ค.
teamRepository.findTeamByMember(member.getId())๋ฅผ ์คํฐ๋น ํ ๋ ์ธ์ ๊ฐ์ผ๋ก null์ ๋๊ฒจ์ฃผ๋ฉด ํด๊ฒฐ๋๊ธด ํ์ง๋ง ์ข์ง ์์ ๋ฐฉ๋ฒ ๊ฐ์๋ค.
๊ณ ๋ฏผ๋์ Mockito Spy๋ฅผ ์ฌ์ฉํ๊ธฐ๋ก ํ๋ค.
Mockito์ spy๋ ๊ธฐ์กด ๊ฐ์ฒด๋ฅผ ๊ทธ๋๋ก ๋ค๊ณ ์์ผ๋ฉด์ ํน์ ๋ฉ์๋๋ง ์คํฐ๋น ํ ์ ์๋ ๊ฐ์ฒด์ด๋ค.
์๋์ ๊ฐ์ด member์ id๊ฐ๋ง ์คํฐ๋นํ์ฌ ํด๊ฒฐํ ์ ์์๋ค.
Member spyMember = spy(member);
when(spyMember.getId()).thenReturn(1L);
Querydsl ์ฌ์ฉ ์ DataJpaTest ์คํจ
ํ๋ก์ ํธ์์ querydsl ์ ์ค์ ์ ํด์ฃผ๊ณ test case ์ ์์ฑํ์๋ค. ํ์ง๋ง jpaqueryfactory ๊ฐ bean ์ฃผ์ ์ ๋ฐ์ง ๋ชปํ๋ฉด์ ์๋ฌ๊ฐ ๋ฐ์์ด ๋์๋ค. ์๋ฌ๋ ์๋์ ๊ฐ๋ค.
No qualifying bean of type 'com.querydsl.jpa.impl.JPAQueryFactory' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
TestConfig ํด๋์ค๋ฅผ ๋ฐ๋ก ๋ง๋ค์ด ํด๊ฒฐํด์ฃผ์๋ค.
@TestConfiguration : test case ์์ ์ ์ค์ ์ ์ฃผ์ ์์ผ์ฃผ๋ ์ด๋ ธํ ์ด์
@PersistenceContext : ์ด ์ด๋ ธํ ์ด์ ์ ํตํด์ EntityManager ์์์ฑ์ ์ฃผ์ ์์ผ์ค.
@TestConfiguration
public class TestConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory(){
return new JPAQueryFactory(entityManager);
}
}
๋ง๋ TestConfig๋ ํ ์คํธ ์ฝ๋์์ @Import ํด์ ์ฌ์ฉํ ์ ์๋ค.
Spring Security ์ฌ์ฉ ์ ํตํฉ ํ ์คํธ ์ฝ๋์์์ ์ธ์ฆ, ์ธ๊ฐ
์ปจํธ๋กค๋ฌ ํ ์คํธ๋ฅผ ํ๊ธฐ ์ํด ํตํฉํ ์คํธ ์์ฑ์ค์ ์ํ๋ฆฌํฐ๋ฅผ ์ฌ์ฉํ ์ฝ๋๊ฐ ์์ด ์๋ฌ๊ฐ ํฐ์ก๋ค.
exception: java.lang.NullPointerException: Cannot invoke "com.tickerBell.global.security.context.MemberContext.getMember()" because "memberContext" is null
์ปจํธ๋กค๋ฌ ์ฝ๋์ ์ผ๋ถ์ด๋ค.
์ปจํธ๋กค๋ฌ ์ฝ๋์์ @AuthenticationPrincipal ์ ์ฌ์ฉํด ํ์ฌ ๋ก๊ทธ์ธ ํ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋๋ฐ ์ด๋ null ๊ฐ์ด ๋ค์ด์ค๊ฒ ๋๊ณ null ์ ์ ๊ทผํ๋ ์ฝ๋์์ NullPointerException์ด ํฐ์ง ๊ฒ์ด๋ค.
๊ทธ๋ ๋ค๊ณ SpringSecurity๋ฅผ ํตํด์ ์ ๊ทผ์ ์ด๋ฅผ ํด๋์ URL๋ก Controller Test๋ฅผ ํ ๋ ๋งค๋ฒ ์ธ์ฆ๋ก์ง(๋ก๊ทธ์ธ, ํ ํฐ..)์ ๋ถ์ฌ์ฃผ๋๊ฑด ํจ์จ์ด ๋งค์ฐ ๋จ์ด์ก๋ค.
์ด๋ @withUserDetailsํ์ฉํ๋ฉด Test์์ header์ token์ ๋ฃ์ด์ฃผ์ง ์์๋ ๊ฐํธํ๊ฒ ํ ์คํธ ํ ์ ์๋ค.
@WithUserDetails
@withUserDetails๋ UserDetailsService๋ฅผ ํตํด์ User๊ฐ์ฒด๋ฅผ ์ง์ ๋ฐํํ๊ธฐ ๋๋ฌธ์ ๋์ฒ๋ผ ๊ฐ์ฒด๊ฐ ์ง์ ํ์ํ ๋๋ ์ด ๋ฐฉ๋ฒ์ ์ฐ๋๊ฒ ์ข์ ๊ฒ ๊ฐ๋ค.
value๋ ์ด๋ค username์ ๊ฐ์ user๋ฅผ SecurityContextHolder์ ๋ฃ์์ง userDetailsServiceBeanName์ ์ด๋ค UserDetailsService๋ฅผ ์ด์ฉํ ์ง ์๋ ค์ฃผ๋ ๊ฒ์ด๋ค. ๋ง์ฝ Custom์ ํ๋ค๋ฉด ๊ทธ๊ฒ์ ์ ์ด์ฃผ๋ฉด๋๋ค. ๋๋ ๋ฐ๋ก ์ปค์คํ ํ์ง ์์๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก ์ ์ด์ฃผ์ง ์์๋ค.
ํ์ง๋ง ์ด๋ value๋ ์ค์ ๋ก ํด๋น username์ ๊ฐ์ง user๊ฐ ์กด์ฌํด์ผํ๋ค.
๋ฐ๋ผ์ Test์งํ์ ์ user๊ฐ์ฒด๋ฅผ ๋ฃ์ด๋์ผํ๊ธฐ๋๋ฌธ์ @BeforeEach๋ฅผ ํตํด์ user๋ฅผ ์ ์ฅํด๋์ผ๋ฉด ๋๋ค.
๋ ๋ค๋ฅธ ์ด์!
SecurityContext๋ default๋ก TestExecutionListener.beforeTestMethod๋ก ์ค์ ์ด ๋์ด์๋ค.
์ฆ @Before ์ ์ @withUserDetails์ด ๋์ํด์ SecurityContext์์ ๋ฃ์ผ๋ ค๊ณ ํ๋ค.
๋ฐ๋ผ์ ์ค user๊ฐ์ฒด๊ฐ ์๊ธฐ๊ธฐ์ ์ ํด๋น user๋ฅผ ์ฐพ๊ฒ ๋๊ณ ์คํจํ๊ฒ ๋๋ค.
ํด๊ฒฐ๋ฒ์ผ๋ก ์๋ ์ต์ ์ ์ถ๊ฐํด์ฃผ๋ฉด ๋๋ค.
setupBefore = TestExecutionEvent.TEST_EXECUTION)