우테코 1주차 금요일, 문자열 덧셈 계산기 미션에 대한 피드백을 받으며 TDD에 대해 강의를 들을 수 있었다. TDD란 테스트 주도 개발의 약자로 이름에서도 알 수 있듯이 테스트를 중심으로 개발하는 방법이다. 기본적인 흐름으로는 위 그림처럼 실패하는 테스트를 먼저 짠 뒤에 구현을 해서 테스트를 통과시킨 다음 리펙토링을 한다. 이번 포스팅의 내용은 강의를 바탕으로 Junit, java를 사용하여 TDD를 하는 방법에 대해 정리한 것이다.
테스트 코드부터 짠다.
구현을 위한 프로덕션 코드를 구현하기 전에 실패하는 테스트 코드부터 작성한다. 아직 만들어지지 않은 클래스나 함수가 있어 컴파일 에러가 날 것이다.
테스트 코드 하나 만들고 프로덕션 코드를 작성하러 간다
여러 테스트 코드를 한 번에 만들고 프로덕션 코드를 구현하러 가는게 아니다. 최소한의 단위로 테스트 코드 -> 프로덕션 코드 순서를 지키자.
최소한의 작업으로 테스트가 통과되도록 구현한다.
','이이나 ':'으로 나누어진 정수 문자열을 받아 모두 합하는 계산기를 구현한다고 가정하자.
구현 기능 목록 중에 제일 먼저 구현할 내용이 'null이 들어오면 0을 반환한다'라고 했을 때 테스트 코드는 다음과 같을 것이다.
@Test
void splitAndSum_null() throws Exception {
int result = StringCalculator.splitAndSum(null);
assertThat(result).isEqualTo(0);
}
이 테스트를 통과시키기 위해 splitAndSum() 함수를 구현한다고 했을 때 어떻게 해야 할까? 최소한의 작업으로 테스트가 통과하려면 이렇게 짜야할 것이다.
public static int splitAndSum(final String input) {
return 0;
}
뭔가 이상한 느낌도 든다. 이렇게 하면 null뿐만 아니라 어떤 입력이 들어오더라도 상관 없이 0이 반환될 것이다. 하지만 작성해두었던 테스트는 통과한다.
이제 다음 요구사항인 숫자 하나가 들어오면 그 숫자를 그대로 반환한다를 만족시키기 위해 테스트 코드를 짜보자.
@Test
void splitAndSum_숫자하나() throws Exception {
int result = StringCalculator.splitAndSum("1");
assertThat(result).isEqualTo(1);
}
이제 또 구현하러 가야 하는데 여기서 주의할 점은 이전에 통과시켰던 테스트도 같이 만족시켜야 한다는 것이다.
(그냥 return Integer.parseInt(input)한다면 최소한의 작업이지만 기존의 테스트가 깨져버림)
public static int splitAndSum(String input) {
if (input == null) {
return 0;
}
return 1;
}
이렇게 하면 두 테스트 모두 통과되는 것을 확인할 수 있다. 지금은 코드가 간단해서 리펙토링 할 것도 없어 보이지만 테스트가 통과하면 적절히 리펙토링 해준다. (레드 그린 리펙토링)
이런 방식으로 모든 요구사항을 구현하고 모든 테스트가 통과한다면 구현이 완료되는 것이다.
그런데 뭔가 이상하다. 위 구현 코드는 숫자 하나가 "1"인 경우만 상정하고 있다.
테스트 메서드를 촘촘하게 짠다.
여러 상황에서도 통과되도록 테스트 코드를 짜야한다. 위의 의문에서도 알 수 있듯이 "1"을 받으면 1을 반환한다는 상황에서만 테스트가 통과하기 때문에 "2"를 받으면 2를 반환한다는 테스트를 짜면 실패할 것이다. 각 입력마다 여러 테스트 메서드를 짜도 되지만 @ParameterizedTest를 이용하는 방법도 있다. 이렇게 하면 "1"이 들어오든 "100"이 들어오든 원하는 결과를 반환한다.
테스트 코드
@ParameterizedTest
@ValueSource(strings = {"1", "2", "100"})
void splitAndSum_숫자하나(String input) throws Exception {
int result = StringCalculator.splitAndSum(input);
assertThat(result).isEqualTo(Integer.parseInt(input));
}
프로덕션 코드
public static int splitAndSum(String input) {
if (input == null) {
return 0;
}
return Integer.parseInt(input);
}
assert문은 테스트당 하나만!
테스트를 촘촘하게 한다고 하나의 테스트 메서드에 assert 문이 여러 개인 것은 좋지 않다. 테스트가 실패했을 때 어떤 assert로 인해 실패했는지 한 번에 알기 힘들다.
적절한 시점에 커밋을 끊는 게 좋다.
상황에 따라 다르겠지만 하나의 기능을 구현할 때 테스트 + 구현 + 리펙토링 후 커밋 (각각에 다 커밋할 필요 없음)
어려운 부분을 조금 고심을 해서 구현해야 할 때 팁
구현 전에 테스트 코드 밑에서 어려운 부분을 테스트로 작성해 시험해본다.
@Test
void 대충_고심해야하는_테스트_코드() throws Exception {
//...
}
@Test
void 고심할_부분_시험해_보자() throws Exception {
// 이것저것 해봄
}
이처럼 바로 프로덕션 코드로 넘어가서 구현하는 것이 아니라 어느 정도 생각과 방법을 정리한 뒤 넘어가면 좋다.
리펙토링 할 때 팁
조금 복잡한 코드라면 테스트가 통과한 뒤 리펙토링할 필요가 있다. 리펙토링을 하다 보면 다른 테스트가 깨질 수도 있고 이미 고친 뒤라서 원복 하는 복잡함이 있을 수도 있다. 그래서 고치려는 메서드를 복사한 새로운 공간에서 고치고 테스트 통과 확인하고 원래 메서드에 복붙 하면 좋다.
그밖에 팁!
@DisplayName과 @SupperessWarning
테스트 메서드 이름은 한글로 남기면 아스키가 아니라고 warn이 뜬다. 작은 프로젝트는 상관없지만 이러한 warn을 실무에서는 다 체크하고 쌓이면 안 된다. 때문에 이름은 영어로 쓰고 @DisplayName을 통해 한글명을 입력해주면 좋다.
클래스명 위에 @SupperessWarnings(“NonAschCharacters")로 아스키 관련 warn을 꺼주고 메서드명을 한글로 써도 됨.
var
자바는 강타입 언어로 타입 선언을 변수 전에 적어줘야 하는데 jdk11부터는 타입 추론 “var”가 추가되어 자동으로 타입을 알맞게 변환해준다. 어떤 함수가 어떤 타입을 리턴하는지 애매한 상황에 도움이 된다. var 타입을 쓰느냐 마느냐는 취향 차이라고 한다. 인텔리제이 단축키인 ctrl + alt + v(맥에서는 cmd + option + v)로 알아서 알맞은 타입을 빼주면서 코딩하고 있는데 아직은 이 쪽이 더 편한 느낌이다.
final 사용에 대해
요즘 언어는 다 불변 언어, 자바는 가변 언어. 변하지 않을(or 변하면 안 되는) 변수에는 final을 붙여 변하지 않았음을 확신하여 마음의 안정을 얻을 수 있다.
'방법론' 카테고리의 다른 글
소프트웨어와 복잡성 관리 (0) | 2024.09.01 |
---|---|
도메인 vs 엔티티 (feat. DDD) (1) | 2022.05.15 |
전략패턴과 OCP (2) | 2022.02.18 |
MVC(Model-View-Controller) 패턴이란 (0) | 2022.02.15 |