ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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);
        }
    }

    댓글

Designed by Tistory.