-
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); } }