앞번의 암호 강도 측정기능의 테스트 예시 에서 보면,

테스트 코드를 작성한 순서는 다음과 같다.

  1. 모든 규칙을 충족하는 경우
  2. 길이만 8글자 미만이고 나머지 조건은 충족하는 경우
  3. 숫자를 포함하지 않고 나머지 조건은 충족하는 경우
  4. 값이 없는 경우
  5. 대문자를 포함하지 않고 나머지 조건을 충족하는 경우
  6. 길이가 8글자 이상인 규칙만 충족하는 경우
  7. 숫자 포함 조건만 충족하는 경우
  8. 대문자 포함 조건만 충족하는 경우
  9. 아무 조건도 충족하지 않는 경우

위의 순서는 다음 규칙을 따른 것이다.

  • 쉬운 경우 → 어려운 경우
  • 예외적인 경우 → 정상적인 경우

초반에 복잡한(어려운) 테스트부터 시작하면 안 되는 이유

만약에 테스트 코드 작성 순서를 다음과 같이 진행했다고 하자. 

  1. 대문자 포함 조건만 충족하는 경우
  2. 모든 규칙을 충족하는 경우
  3. 숫자를 포함하지 않고 나머지 규칙은 충족하는경우

이렇게 진행하다보면 첫 번째 테스트는 쉽게 넘어가지만, 두 번째는 모든 규칙을 검증하는 코드를 구현해야 할것 같은 느낌이 든다.

 

한번에 완벽한 코드를 만들다 보면

▶ 나도 모르게 버그를 만든다.

    → 버그를 잡는데 많은 시간을 허비한다.

    → 테스트 통과 시간도 길어진다

→ 집중력이 떨어진다

 

구현하기 쉬운 테스트부터 시작하기

암호 강도 측정기능의 테스트 를 한다고 하면 가장 쉬워보이는 것은 다음과 같다.

  • 모든 조건을 충족하는 경우
  • 모든 조건을 충족하지 않는 경우

두 가지 경우다 그냥 단순히 해당 값을 리턴하면 되기 때문이다.

  • 모든 조건을 충족하는 경우  → return STRONG
  • 모든 조건을 충족하지 않는 경우 → return WEEK

먼저 "모든 조건을 충족하는 경우"  부터 시작했다고 하면, 다음으로 생각해 볼수 있는 테스트는 다음과 같다.

  • 모든 조건을 충족하지 않는 경우 → 모든 조건을 검증하는 코드 필요
  • 한 가지 조건만 충족하는 경우 → 한 가지 규칙을 충족하는지 검증하는 코드 필요
  • 두 가지 조건을 충족하는 경우 두 가지 규칙을 충족하는지 검증하는 코드 필요

이렇게 보면 "한 가지 조건만 충족하는 경우" 가 더 구현하기 쉽다.

 

이런 식으로 한 가지 테스트를 통과했으면 그 다음으로 구현하기 쉬운 테스트를 선택하여 진행해야 한다.

 

예외 상황을 먼저 테스트해야 하는 이유

초기 설계 단계에서 예외 처리를 고려하는 것은 프로그래밍의 중요한 부분

 

예외 처리를 무시하고 코드를 작성할 경우,

나중에 예외 상황을 반영하기 위해 코드 전체를 개편해야 할 수도 있음

복잡한 조건문을 추가 해야 할 수도 있음

코드의 복잡성을 증가시키고 버그 발생 가능성 상승

 

완급 조절

한번에 얼마만큼의 코드를 작성할 것인가?

  1. 정해진 값을 리턴
  2. 값 비교를 이용해서 정해진 값을 리턴
  3. 다양한 테스트를 추가하면서 구현을 일반화

뻔한 구현이라도 위 단계를 거쳐서 연습을 하면,

구현이 막막해도 조금씩 기능을 구현해 나갈 수 있다.

 

리팩토링?  ▶  코드의 가독성 향상

코드 중복 발생, 코드가 길어진다

▶ 메서드 추출과 같은 기법을 이용하여 리팩토링  

▶ 메서드 이름으로 코드의 의미 표현

 

 

리팩토링

1. 동작하는 코드를 먼저 작성

2. 변경하기 쉬운구조로 코드를 구성

3. 필요한 부분은 리팩토링

4. 메서드의 구조에 영향을 주는 리팩토링은 큰 틀에서 구현 흐름이 눈에 들어오기 시작한 뒤에 진행 (코드의 의미와 구조가 명확해지만 리팩토링 진행)

5. 범위가 큰 리팩토링은 시간이 오래 걸리므로 먼저 테스트를 통과 시키는데 집중

 

테스트

1. 테스트할 목록을 미리 정하기

2. 정한 테스트 목록중 구현이 쉽고 예외적인지 예측

3. 새로운 테스트 사례는 바로 목록에 기록

4. 목록을 하나씩 통과하면서 진행한다.

 

구현이 막막할때

1. 검증하는 코드부터 작성해 본다.

 

구현이 막힐때

1. 과감하게 지우고 다시 시작해보자

검사할 규칙

  • 길이가 8글자 이상
  • 0부터 9사이의 숫자를 포함
  • 대문자 포함
  • 세 규칙을 모두 충족하면 암호는 강함이다.
  • 2개의 규칙을 충족하면 암호는 보통이다.
  • 1개 이하의 규칙을 충족하면 암호는 약함이다.

첫 번째 테스트: 모든 규칙을 충족하는 경우

Test 코드

package chap02;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class PasswordStrengthMeterTest {

    @Test
    void meetsAllCriteria_Then_Strong(){
        PasswordStrengthMeter meter = new PasswordStrengthMeter();
        PasswordStrength result = meter.meter("ab12!@AB");
        assertEquals(PasswordStrength.STRONG, result);
    }
}

 

암호검사기 코드

 

PasswordStrength

package chap02;

public enum PasswordStrength {
    STRONG
}

 

PasswordStrengthMeter

 

1 ) PasswordStrengthMeter 클래스를 작성하여 컴파일 에러를 먼저 해결

package chap02;

public class PasswordStrengthMeter {
    public PasswordStrength meter(String s) {
        return null;
    }
}

▶▶ 테스트 실행시 null 을 반환하므로 실패 

 

2) 테스트를 성공시키기 위해서 STRONG 을 반환하도록 수정

package chap02;

public class PasswordStrengthMeter {
    public PasswordStrength meter(String s) {
        return PasswordStrength.STRONG;
    }
}

 

▶▶ 테스트 성공

 

3) 테스트 코드에 모든 규칙을 충족하는 예를 하나 더 추가

package chap02;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class PasswordStrengthMeterTest {

    @Test
    void meetsAllCriteria_Then_Strong(){
        PasswordStrengthMeter meter = new PasswordStrengthMeter();
        PasswordStrength result = meter.meter("ab12!@AB");
        assertEquals(PasswordStrength.STRONG, result);

        PasswordStrength result2 = meter.meter("abc1!ABD");
        assertEquals(PasswordStrength.STRONG, result2);
    }
}

▶▶ 테스트 성공

 

위와 같은 방식을 따라

아래와 같이 (가능한 모든)조건을 추가해가면서

즉, 검증하는 범위를 추가해가면서 코드를 완성하면 된다.

  • 두 번째 테스트 - 길이만 8글자 미만이고 나머지 조건은 충족하는 경우
  • 세 번째 테스트 - 숫자를 포함하지 않고 나머지 조건은 충족하는 경우
  • 네 번째 테스트 - 값이 없는 경우
  • 다섯 번째 테스트 - 대문자를 포함하지 않고 나머지 조건을 충족하는 경우
  • 여섯 번째 테스트 - 숫자 포함 조건만 충족하는 경우
  • 일곱 번째 테스트 - 숫자 포함 조건만 충족하는 경우
  • 여덟 번째 테스트 - 대문자 포함 조건만 충족하는 경우
  • 아홉 번째 테스트 - 아무 조건도 충족하지 않는 경우

※ 테스트 하는 도중에 지속적으로 코드 정리(리팩토링)도 필요하다.

  • 테스트 성공 후 리펙토링할 대상이 보이면리팩토링
  • 테스트 성공 후 리펙토링할 대상이 안보이면 ▷ 다음 테스트 진행

리팩토링시 가독성을 높이는 것이 목표

  • 처음보는 개발자도 알 수 있게 수정
  • 전반적인 로직을 알기 쉽게 수정

TDD 란?

먼저 테스트를 하고 그다음에 구현하는 방식.

 

테스트를 먼저 작성하고 테스트에 실패하면
테스트를 통과 시킬 만큼 코드를 추가하는 과정을 반복하면서
점진적으로 기능을 완성해 나가는 방식을 추구한다.

 

TDD 기본흐름 예시

덧셈 기능을 검증하기 위한 테스트

 

CalculatorTest.java

package chap02;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class CalculatorTest {
    @Test
    void plus() {
        int result = Calculator.plus(1, 2);
        assertEquals(3, result);
    }
}

 

 

step 1 ) 클래스 생성하고 0을 리턴하는 plus() 메서드를 추가

Calculator.java

package chap02;

public class Calculator {
    public static int plus(int a1, int a2) {
        return 0;
    }
}

 → CalculatorTest.java 실행 → return 값이 0 이여서 테스트 실패 

 

step 2 ) 테스트를 통과하기 위해 3 을 return 

Calculator.java

package chap02;

public class Calculator {
    public static int plus(int a1, int a2) {
        return 3;
    }
}

 → CalculatorTest.java 실행 → 테스트 성공

 

step 3) 추가 검증코드를 추가

CalculatorTest.java

package chap02;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class CalculatorTest {
    @Test
    void plus() {
        int result = Calculator.plus(1, 2);
        assertEquals(3, result);
        assertEquals(5, Calculator.plus(4, 1));
    }
}

 

Calculator.java

package chap02;

public class Calculator {
    public static int plus(int a1, int a2) {
        return a1 + a2;
    }
}

 → CalculatorTest.java 실행 → 테스트 성공

문제(출처: 프로그래머스)

(문제 내용)
문자열 my_string과 is_prefix가 주어질 때, is_prefix가 my_string의 접두사라면 1을, 아니면 0을 return 하는 solution 함수를 작성해 주세요.

 

▶ 내가 푼 방식

//내가 작성한 코드
function solution(my_string, is_prefix) {
    return my_string.startsWith(is_prefix) ? 1: 0;
}

 

 다른 유저가 푼 방식

// 유저 1  
function solution(my_string, is_prefix) {
    return my_string.slice(0, is_prefix.length) === is_prefix ? 1 : 0;
}

// 유저 2
function solution(my_string, is_prefix) {
    return my_string.substring(0, is_prefix.length) === is_prefix ? 1 : 0;
}

// 유저 3

 

 배운 것들

     - 문자열.startWith('특정 문자열') : 문자열이 특정 문자열로 시작하는지 여부를 확인해주는 메서드

     - 

문제(출처: 프로그래머스)

(문제 내용)
정수 n과 k가 주어졌을 때, 1 이상 n이하의 정수 중에서 k의 배수를 오름차순으로 저장한 배열을 return 하는 solution 함수를 완성해 주세요.

 

▶ 내가 푼 방식

//내가 작성한 코드
function solution(n, k) {
    var answer = [];
    for(i = k; i<=n; i += k){
        answer.push(i)
    }
    return answer;
}

 

 다른 유저가 푼 방식

// 유저 1
function solution(n, k) {
    var answer = [];
    for (let i = k; i <= n; i++) {
        if (i % k == 0) {
            answer.push(i);
        }
    }
    return answer;
}

 

 배운 것들

     - 

     - 

+ Recent posts