프로그래밍
TDD(Test-Driven Development) 공부 정리
노력의천재
2022. 9. 7. 22:15
TDD(Test-Drivent Development)란?
테스트 주도 개발, 테스트로부터 시작하는 개발 방식을 의미한다.
- 실패하는 테스트 코드를 작성하고,
- 테스트를 통과시킬 만큼 구현한 후,
- 리팩토링하는 작업을 반복한다.
TDD의 장점
- 테스트 코드가 있으면 문제 범위를 좁혀서 디버깅하는게 수월하다.
- 리팩토링 등 코드를 수정할 때 실패하는 테스트 코드가 발생하면 문제를 빨리 찾아낼 수 있다.
- 테스트가 가능하려면 의존 대상을 대역(Mock)으로 교체할 수 있어야 하는데, 대역으로 교체할 수 있는 구조는 역할별로 잘 분리되어 있는 설계 구조를 가졌다고 볼 수 있다.
예제 코드 1
/**
* 암호화 검사기 TDD 예제
*
* 사용하는 규칙
* - 길이가 8글자 이상
* - 0 ~ 9 사이 숫자 포함
* - 대문자 포함
* 세 규칙을 모두 충족하면 '강함'
* 두 규칙을 충족하면 '보통'
* 하나 이하의 규칙을 충족하면 '약함'
*
* 시연 진행 순서
* 1. 테스트 클래스와 메서드 작성, 실행
* 2. null 입력에 대한 테스트로 시작 (테스트 대상 타입 정의, 메서드 정의, 결과 타입 정의)
* 3. 빈 값에 대한 테스트
* 4. 모든 조건을 충족하는 테스트
* 5. 두 조건을 충족하는 테스트
* 6. 한 조건을 충족하는 테스트
* 7. 아무 조건도 충족하지 않는 테스트
*/
class PasswordMeterTest {
@DisplayName("null 입력에 대한 테스트")
@Test
void nullInputTest() {
assertPasswordStrength(null, PasswordStrength.INVALID);
}
@DisplayName("빈 값에 대한 테스트")
@Test
void emptyInputTest() {
assertPasswordStrength("", PasswordStrength.INVALID);
}
@DisplayName("모든 조건을 충족하는 테스트")
@Test
void meetAllRulesTest() {
assertPasswordStrength("abcdABCD123", PasswordStrength.STRONG);
assertPasswordStrength("123ABCD123", PasswordStrength.STRONG);
assertPasswordStrength("abcd123ABCD", PasswordStrength.STRONG);
}
@DisplayName("두 조건을 충족하는 테스트(길이 규칙 위반)")
@Test
void meet2RulesExceptForLengthRuleTest() {
assertPasswordStrength("abc12AB", PasswordStrength.NORMAL);
assertPasswordStrength("AB12a", PasswordStrength.NORMAL);
assertPasswordStrength("12ABab", PasswordStrength.NORMAL);
}
@DisplayName("두 조건을 충족하는 테스트(숫자 규칙 위반)")
@Test
void meet2RulesExceptForDigitRuleTest() {
assertPasswordStrength("abcdABCD", PasswordStrength.NORMAL);
assertPasswordStrength("ABCDabcd", PasswordStrength.NORMAL);
assertPasswordStrength("abABcdCD", PasswordStrength.NORMAL);
}
@DisplayName("두 조건을 충족하는 테스트(대문자 규칙 위반)")
@Test
void meet2RulesExceptForUpperCaseRuleTest() {
assertPasswordStrength("abcd1234", PasswordStrength.NORMAL);
assertPasswordStrength("abcd1234", PasswordStrength.NORMAL);
assertPasswordStrength("ab12cd34", PasswordStrength.NORMAL);
}
@DisplayName("오직 한 조건을 충족하는 테스트")
@Test
void meetOnlyLengthRuleTest() {
assertPasswordStrength("abcdefghijk", PasswordStrength.WEAK);
assertPasswordStrength("abcdefgh", PasswordStrength.WEAK);
assertPasswordStrength("abcd!@#$%", PasswordStrength.WEAK);
}
@DisplayName("아무 조건도 충족하지 않는 테스트")
@Test
void meetNoRuleTest() {
assertPasswordStrength("abc", PasswordStrength.WEAK);
assertPasswordStrength("abcd", PasswordStrength.WEAK);
assertPasswordStrength("abcd!@", PasswordStrength.WEAK);
}
private void assertPasswordStrength(String password, PasswordStrength expected) {
PasswordMeter passwordMeter = new PasswordMeter();
PasswordStrength result = passwordMeter.meter(password);
assertThat(result).isEqualTo(expected);
}
}
public class PasswordMeter {
private final Pattern digitPattern = Pattern.compile("[0-9]");
private final Pattern upperCasePattern = Pattern.compile("[A-Z]");
public PasswordStrength meter(String password) {
if (password == null || password.isEmpty()) {
return PasswordStrength.INVALID;
}
int meetCnt = getMeetCnt(password);
if (meetCnt == 0 || meetCnt == 1) {
return PasswordStrength.WEAK;
}
if (meetCnt == 2) {
return PasswordStrength.NORMAL;
}
return PasswordStrength.STRONG;
}
private boolean hasDigit(String password) {
return digitPattern.matcher(password).find();
}
private boolean hasUpperCase(String password) {
return upperCasePattern.matcher(password).find();
}
private boolean meetLength(String password) {
return password.length() >= 8;
}
private int getMeetCnt(String password) {
int meetCnt = 0;
if (meetLength(password)) {
meetCnt++;
}
if (hasDigit(password)) {
meetCnt++;
}
if (hasUpperCase(password)) {
meetCnt++;
}
return meetCnt;
}
}
예제 코드 2
/**
* 회원 승인 API
*
* 대기 상태의 회원을 승인하면 회원 상태로 활성화 됨
*
* / - MemberRepository - MemoryMemberRepository
* API - ConfirmMemberService
* \ - Member
*
*/
@SpringBootTest
@AutoConfigureMockMvc
public class MemberApiIntegrationTest {
@Autowired
private MemberRepository memberRepository;
@Autowired
private MockMvc mockMvc;
@DisplayName("")
@Test
void confirm() throws Exception {
// arrange (given)
memberRepository.save(new Member("id", MemberStatus.WAITING));
// act (when)
mockMvc.perform(post("/members/{id}/confirm", "id"))
.andExpect(status().isOk());
// assert (then)
Member m = memberRepository.findById("id");
assertThat(m.getStatus()).isEqualTo(MemberStatus.ACTIVE);
}
}
@RestController
public class MemberApi {
private final ConfirmMemberService confirmMemberService;
public MemberApi(ConfirmMemberService confirmMemberService) {
this.confirmMemberService = confirmMemberService;
}
@PostMapping("/members/{id}/confirm")
public ResponseEntity<?> confirm(@PathVariable("id") String id) {
confirmMemberService.confirm(id);
return ResponseEntity.ok("OK");
}
}
@MockBean(JpaMetamodelMappingContext.class)
@WebMvcTest(MemberApi.class)
public class MemberApiTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ConfirmMemberService confirmMemberService;
@DisplayName("")
@Test
void shouldCallService() throws Exception {
// when(테스트 실행)
mockMvc.perform(post("/members/{id}/confirm", "id"))
.andExpect(status().isOk());
// then(검증)
BDDMockito.then(confirmMemberService)
.should()
.confirm("id");
}
}
@Service
public class ConfirmMemberService {
private final MemberRepository memberRepository;
public ConfirmMemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public void confirm(String id) {
Member member = memberRepository.findById("id");
if (member == null) {
throw new MemberNotFoundException();
}
member.confirm();
}
}
public class ConfirmMemberServiceTest {
public static final String ID = "id";
private MemberRepository memberRepository = new MemoryMemberRepository();
private ConfirmMemberService confirmMemberService = new ConfirmMemberService(memberRepository);
@BeforeEach
public void initData() {
memberRepository.deleteAll();
}
@DisplayName("")
@Test
void noMember() {
Assertions.assertThatCode(() -> {
confirmMemberService.confirm(ID);
}).isInstanceOf(MemberNotFoundException.class);
}
@DisplayName("")
@Test
void memberAleadyActivated() {
memberRepository.save(new Member(ID, MemberStatus.ACTIVE));
Assertions.assertThatCode(() -> {
confirmMemberService.confirm(ID);
}).isInstanceOf(MemberAlreadyActivatedException.class);
}
@DisplayName("")
@Test
void confirm() {
// arrange (given)
memberRepository.save(new Member(ID,MemberStatus.WAITING));
// act (when)
confirmMemberService.confirm(ID);
// assert (then)
Member m = memberRepository.findById(ID);
assertThat(m.getStatus()).isEqualTo(MemberStatus.ACTIVE);
}
}