- 문자열을 검사해서 규칙을 준수하는지에 따라 암호를
약함
,보통
,강함
으로 구분 - 검사할 규칙은 다음 세 가지
- 길이가 8글자 이상
- 0부터 9 사이의 숫자를 포함
- 대문자 포함
- 세 규칙을 모두 충족하면 암호는
강함
- 2개의 규칙을 충족하면 암호는
보통
- 1개 이하의 규칙을 충족하면 암호는
약함
- 첫 번째 테스트를 선택할 때에는 가장 쉽거나 가장 예외적인 상황을 선택해야한다.
-
테스트 코드도 코드이기 때문에 유지보수 대상이다.
-
즉, 테스트 메서드에서 발생하는 중복을 알맞게 제거하거나 의미가 잘 드러나게 코드를 수정할 필요가 있다.
-
테스트 코드의 중복을 무턱대고 제거하면 안 된다. 중복을 제거한 뒤에도 테스트 코드의 가독성이 떨어지지 않고 수정이 용이한 경우에만 중복을 제거해야한다.
- 중복을 제거한 뒤에 오히려 테스트 코드 관리가 어려워진다면 제거했던 중복을 되돌려야 한다.
public class Main
{
public static void main(String[] args)
{
System.out.println( "ABC".isBlank() ); //false
System.out.println( " ".isBlank() ); //true
System.out.println( "ABC".isEmpty() ); //false
System.out.println( " ".isEmpty() ); //false
}
}
다른 예외까지 처리하려고 하는데.. 지금 당장은 테스트케이스만 통과되도록 코드를 작성하자.
고민했던 건 아래 코드인데,
if(!lengthEnough){
return PasswordStrength.NORMAL;
}
!길이 and (!숫자 또는 !대문자) 일 경우 WEAK에 대한 예외가 안돼있어서 처리해줘야 하나? 였음
하지만, 당시 작성중이었던 테스트 코드는 길이가 8글자 이상인 조건만 충족하는 경우
였으므로,
위 예외는 생각할 필요가 없음.
즉, 지금은 이 부분만 작성해주면 됨
if(lengthEnough && !containNum && !containUppercase){
return PasswordStrength.WEAK;
}
-
TDD 사이클은
Red-Green-Refactor
- 레드는 실패하는 테스트
-
테스트가 개발을 주도
- 테스트를 작성하는 과정에서 구현을 생각하지 않음
- 해당 기능이 올바르게 동작하는지 검증할 수 있는 테스트 코드를 만듬
- 테스트를 통과시킬 만큼 기능을 구현
- 아직 추가하지 않은 테스트를 고려해서 구현하지 않음
- 테스트를 작성하는 과정에서 구현을 생각하지 않음
-
TDD 이점
-
온전하게 동작한다는 것을
검증
해주는 테스트가 있으므로 코드 수정에 대한심리적 불안감
을 줄여준다. -
지속적으로 코드 정리를 하므로, 코드 품질이 급격히 나빠지지 않게 막아주는 효과가 있음
- 향후 유지보수 비용을 낮추는데 기여
-
피드백이 빠르다.
- 새로운 코드를 추가하거나, 기존 코드를 수정하면 테스트를 돌려서 해당 코드가 올바른지 바로 확인할 수 있
- 잘못된 코드가 배포되는 것을 방지
- 새로운 코드를 추가하거나, 기존 코드를 수정하면 테스트를 돌려서 해당 코드가 올바른지 바로 확인할 수 있
-
- 서비스를 사용하려면 매달 1만원을 선불로 납부한다.
- 2개월 이상 요금을 납부할 수 있다.
- 10만 원을 납부하면 서비스를 1년 제공한다.
-
요구사항은 단순 3 가지 이지만, 그외 생각 해야할 예외 케이스의 도출은 연습이 많이 필요한 것 같다.
- 책에 있는 케이스들을 따라해보았지만, 요구사항만 주고 혼자서 해보라 하면 글쎄..아직은 많이 어색하다.
-
아주 작은 단위인 1만원 로직부터 시작해서 예외 상황 처리 해주면서 개발해가는 방식이 인상 깊었다.
-
리팩토링을 할 때, 기존 코드를 바로 수정하는 게 아니라 새로운 코드를 추가한 뒤, 호출 방식으로 정상 동작 하는지 확인 한뒤에 인라인 처리하는 게 인상깊었다.
- 일단 동작하는 코드를 만드는 능력은 중요하다. 코드가 동작하지 않으면 아무것도 소용 없기 때문이다.
- 하지만,
소프트웨어 생존 시간
이 길어질수록 소프트웨어를 지속적으로개선
해야 한다.- 즉, 코드를 변경해야 한다.
- 코드 변경이 어려우면 변화하는 여구를 제때 반영할 수 없게 되며, 이는 소프트웨어의 생존과 직결된다.
- 따라서 코드를 잘 변경할 수 있는 능력 또한 매우 중요하다.
- 코드를 잘 변경하려면,
변경하기 쉬운 구조
를 가져야 하는데 이를 위한 것이 바로리팩토링
이다.
- 코드를 잘 변경하려면,
- TDD를 시작할 때, 테스트할 목록을 미리 정리하면 좋다.
- 그중 구현이 쉬울 테스트가 무엇일지 상상한다.
- 테스트 과정에서 새로운 테스트 사례를 발견하면 즉시 목록 업데이트
- 테스트 목록을 적었다고 해서, 테스트를 한 번에 다 작성하면 안된다.
- 한 번에 작성한 테스트 코드가 많으면 구현 초기에도 리팩토링을 마음 껏 못하게 된다.
- 모든 테스트를 통과시키기 전까지는 계속해서 깨지는 테스트가 존재하므로,
개발리듬
을 유지 하는 데 도움이 안된다.
- 모든 테스트를 통과시키기 전까지는 계속해서 깨지는 테스트가 존재하므로,
- 한 번에 작성한 테스트 코드가 많으면 구현 초기에도 리팩토링을 마음 껏 못하게 된다.
- 하나의 테스트 코드를 만들고 이를 통과시키고, 리랙토링하고.. 짧은 리듬을 반복
- 개발을 진행하다 보면 변경 범위가 매우 큰 리팩토링 거리를 발견할 때도 있다.
- TDD 흐름을 깨기 쉬우므로, 리팩토링 진행을 미뤄두고, 테스트를 통과하는데 집중한다.
-
테스트 코드를 작성하다 보면 시작이 잘 안될 때가 있다.
-
이럴 땐
검증하는 코드부터
작성-
예를 들어 만료일 계산 기능의 경우 만료일을 검증하는 코드부터 작성
@Test void 만원_납부하면_한달_뒤가_만료일이_됨(){ // 처음 작성하는 코드 assertEquals(기대하는 만료일, 실제 만료일); }
-
-
- 과감하게 코드를 지우고 미련없이 다시 시작
- 어떤 순서로 작성했는 지 되돌아보고, 순서를 바꿔 다시 진행
- 상기할 것
- 쉬운 테스트, 예외적인 테스트
- 완급 조절
- 기능은 크게
입력
과결과
로 나뉜다. - 설계는 기능 명세로부터 시작
- 요구사항 문서를 이용해서 기능 명세 구체화
- 구체화하는 동안
입력
과결과
도출 - 도출한 기능 명세를 코드에 반영
- 기능의 이름, 파라미터, 리턴 타입 등 결정됨
- 구체화하는 동안
- 테스트 코드를 만들기 위해 필요한 것
- 테스트에서 실행할 수 있는 객체나 함수의 존재
- 실행 결과를 검증
- 테스트 코드를 작성하는 과정에서 네 가지가 결정됨
- 클래스 이름
- 메서드 이름
- 메서드 파라미터
- 실행 결과
- TDD 자체가 설계는 아니지만, 설계 과정에서 고민하는 것과 겹치는 부분 존재
- 설계가 불필요하게 복잡해지는 것을 방지
- 요구사항 명세에는 개발자가 기능을 구현하기에 생략된 내용이 많다.
- 실무 담당자와 얘기해서 상황에 따라 기능이 어떻게 동작하는지 구체적으로 정리해야 한다.
- @Test 를 붙인 메서드는
private
이면 안된다.- 이유는? 리플렉션 때문인가? 시간되면 찾아보기
- 테스트 메서드가 특정 순서대로 실행된다는 가정하에 테스트 메서드를 작성하면 안된다.
- 각 테스트 메서드는 서로 독립적으로 동작해야 한다.
-
상황 - 실행 - 결과 확인 구조에 너무 집착하지 말자
- 테스트 코드를 보고 테스트 내용을 이해할 수 있으면 다른 구조여도 문제 없음
-
기능은 상황에 따라 실행 결과가 달라진다.
어떤 상황
이 실행 결과에 영향을 줄 수 있는지 찾기 위한 노력이 필요- ex) 파일을 통해 입력을 받는 경우, 외부 API를 이용하는 경우
-
물론, 결과에 영향을 주는 상황이 없는 경우도 존재한다
-
외부 상태가 테스트 결과에 영향을 주지 않게 하기
- 테스트는 언제 실행해도 항상 정상적으로 동작하는 것이 중요하다.
- 외부 환경을 테스트에 알맞게 세팅해야함
- 외부 요인 ex) 파일, DBMS, 외부 서버
-
하지만 테스트에 맞게 외부 환경을 구성하는 것이 항상 가능한 것은 아님
- 테스트 대상의 상황과 결과에 외부 요인이 관여할 경우
대역
을 사용하면 테스트 작성이 쉬워짐 - 대역: 테스트 대상이 의존하는 대상의 실제 구현을 대체하는 구현
- 테스트 대상의 상황과 결과에 외부 요인이 관여할 경우
-
처음 봤을 때 대역을 bandwidth 라고 오해 했는데, 제어하기 힘든 외부의 상황을 흉내내 대신 하는 개념이었음
-
test double에서 double이 대역을 의미
-
의존하는 대상을 구현하지 않아도 테스트 대상을 완성할 수 있게 만들어줌
-
협업 대기 시간을 줄여주어 개발 속도가 향상될 수 있다.
-
대역의 종류
-
Stub
- 구현을 단순한 것으로 대체
- ex) 외부 API를 통한 검증 기능을 단순하게 구현할 수 있음
-
Fake
- 테스트에 필요한 동작하는 구현을 제공
- ex) DB 대신 맵을 이용한 메모리 DB
-
Spy
- 호출된 내역을 기록
- 테스트 결과 검증
- ex) 이메일 발송 여부 확인
-
Mock
- 기대한 상호작용을 하는지 행위 검증
- 스텁이자 스파이
-
-
모의 객체 과하게 사용하지 않기
- 모의 객체를 이용하면 대역 클래스를 만들지 않아도 되니까 처음에는 편할 수 있음
- 결과 값을 확인하는 수단으로 모의 객체를 사용하기 시작하면 결과 검증 코드가 길어지고 복잡해짐
- 모의 객체는 기본적으로 메서드 호출 여부(행위)를 검증하는 수단이기 때문에
- 테스트 대상과 모의 객체 간의 상호 작용이 조금만 바뀌어도 테스트가 깨지기 쉽다.
- 저장소에 대한 대역은 메모리를 이용한 가짜 구현 사용이 테스트 코드 관리에 유리
-
테스트가 어려운 코드
- 하드 코딩된 상수
- ✅ 해결책: 생성자나 메서드 파라미터로 받기
- 의존 객체를 직접 생성
- ❓ 이유: 테스트 하려면 의존하는 모든 환경을 구성해야함
- ex) DB를 준비해야 하고, 필요한 테이블도 만들어야함 -> 테스트 후 제거 작업도 필요
- ✅ 해결책: 의존 대상을 주입 받기
- 생성자, Setter
- ❓ 이유: 테스트 하려면 의존하는 모든 환경을 구성해야함
- 정적 메서드 사용
- ✅ 해결책: 외부 라이브러리는 직접 사용하지 말고 감싸서 사용
// if. 외부 라이브러리가 제공하는 정적 메서드: AuthUtil.authorize(authKey); public class AuthService{ public int authenticate(String id, String pw){ boolean authorized = AuthUtil.auithorize(authKey); if(authorized){ return AuthUtil.authenticate(id,pw); }else{ return -1; } } }
- 실행 시점에 따라 달라지는 결과
- ✅ 해결책: 시간이나 임의 값 생성 기능 분리
- 역할이 섞여 있는 코드
- ✅ 해결책: 테스트 하고 싶은 코드를 분리
- 그 외
- 메서드 중간에 소켓 통신 코드 포함
- 콘솔에서 입력을 받거나 결과를 콘솔에 출력
- 테스트 대상이 사용하는 의존 대상 클래스나 메서드가
final
- 대역으로 대체하기 어려울 수 있음
- 테스트 대상의 소스를 소유하고 있지 않아 수정이 어려운 경우
- 하드 코딩된 상수
-
단위 테스트
-
응용 프로그램에서 테스트 가능한 가장 작은 소프트웨어를 실행하여, 예상대로 동작하는지 확인하는 테스트
-
클래스나 한 메서드와 같은 작은 범위
-
-
통합 테스트
-
단위 테스트가 하지 못하는 DB연결, 외부 api 연동을 통합하며, **비즈니스 로직(내부 구조)**을 테스트 할 수 있는 테스트
-
단위 테스트보다 초기 세팅(디비 커넥션 연결 등..)이 많아 오래 걸린다.
-
결국은 각 구성 요소가 올바르게 연동되는 것을 확인 해야하기 때문에 필요함
-
@SpringBooTest
를 통해 가능
-
-
인수 테스트(= E2E 테스트, 기능 테스트)
- 실제 사용자 관점에서 하는 테스트로, 내부 로직에 관심이 없는
블랙박스 테스트
이다. - 데이터베이스나 외부 서비스에 이르기 까지 모든 구성 요소를 하나로 엮어서 진행한다.
RestAssured
,MockMvc
같은 도구를 활용하여 인수 테스트를 작성할 수 있다.
- 실제 사용자 관점에서 하는 테스트로, 내부 로직에 관심이 없는
-
속도가 빠른 단위 테스트에서 다양한
상황
을 다루고, 통합 테스트나 기능 테스트는주요 상황
에 초점을 맞춰야 함. -
팁
- WireMock 을 이용한 REST 클라이언트 테스트
- 서버 API를 Stub으로 대체할 수 있음
- 스프링 부트의 내장 서버를 이용한 API 기능 테스트
- TestRestTemplate
- WireMock 을 이용한 REST 클라이언트 테스트
-
깨진 테스트가 발견되면 즉시 수정해서 테스트 실패가 확산되는 것을 방지해야한다.
- 깨진 유리창 이론
-
복잡하고, 실수로 테스트가 깨질 수 있다.
-
성공하더라도, 테스트 코드를 처음 보는 사람은 변수와 필드를 오가며 이해해야함
-
뭐가 실패했는지 확인하는 시간이 추가로 듬
-
역할을 분리하자
-
모의 객체는 해당 객체의 내부 로직 테스트 용이 아님
-
테스트 대상의 상황을 원할하게 만들기 위함임
-
내부 구현 검증은 로직이 조금만 변경해도 테스트가 깨질 수 있으므로 지양해야함
-
테스트 코드는 내부 구현보다
실행 결과
를검증
해야한다.
-
테스트가 실패할 경우 실패한 원인을 분석해야한다.
-
모든 곳에서 사용하므로, 테스트가 깨지기 쉬운 구조가 됨.
-
상황 구성 코드가 테스트 메서드 안에 위치해야 함
- 셋업 메서드 중복 상황과 비슷한 사유
- 테스트 메서드에서 직접 상황을 구성하면 복잡해지는 단점이 있는데, 보조 클래스로 코드 중복을 제거한다.
- 꼭 특별한 환경이 필요하다면,
@EnabledOnOs
애노테이션 사용
- ex) 현재 시간 활용
- 검증할 범위에 필요한 값만 설정
- 테스트는 반드시 성공하거나 실패 해야 한다.
- 조건에 따라서 단언을 하지 않으면, 그 테스트는 성공하지도 실패하지도 않은 테스트가 됨
@SpringBootTest
는 서비스, 컨트롤러 등 모든 스프링 빈 초기화 + DB 관련 설정 + etc... = 오래 걸림- DataBase 연동 통합 테스트만 필요한 경우 다른 빈을 생성하지 않는
@JdbcTest
와 같은 애노테이션 이용
- 테스트를 먼저 작성하면 적어도 해당 테스트를 통과한 만큼은 코드를 올바르게 구현했다는 사실을 알 수 있음
- 테스트 코드는
회귀 테스트
로 사용할 수 있음- 코드를 수정하거나 추가할 때 앞서 작성한 테스트를 사용하면 문제가 없는지 바로 확인 가능
- 즉, 버그 수정도 더 쉬워짐.
- 개발시간 =
코딩 + 디버깅 + 테스트
- TDD를 경험하지 못한 사람들은
- 수동으로 테스트하는 시간과 디버깅하는 시간이 개발 시간에 포함된다는 사실을 인지 못하는 경우가 있음
- 테스트 시간을 줄이려면 자동화를 해야하는데, TDD는 이러한 반복적인 테스트 시간을 줄여줌 => 개발시간 감소
- TDD를 경험하지 못한 사람들은
-
테스트 주도 개발 시작하기 - 최범균 저