NEXTSTEP에서 진행하는 교육과정 ATDD와 함께 클린 API로 가는 길 4기다. (글을 작성하는 시점에서는 과정명이 ATDD, 클린 코드 with Spring으로 변경되었다.)
🚀 1단계 - 노선 관리 기능 구현
요구사항
- 아래 인수 조건을 검증하는 인수 테스트 작성하기
- 작성한 인수 테스트를 충족하는 기능 구현하기
예제 시나리오(인수 조건)를 가지고 테스트 코드로 변환하는 작업이다.
제공해주는 깃 레포지토리로 비어있는 코드를 작성하는데 시나리오는 다음과 같은 형식으로 구성되어있다.
Feature: 지하철 노선 관리 기능
Scenario: 지하철 노선 생성
When 지하철 노선 생성을 요청 하면
Then 지하철 노선 생성이 성공한다.
Scenario: 지하철 노선 목록 조회
Given 지하철 노선 생성을 요청 하고
Given 새로운 지하철 노선 생성을 요청 하고
When 지하철 노선 목록 조회를 요청 하면
Then 두 노선이 포함된 지하철 노선 목록을 응답받는다
Scenario : 지하철...
[위와 비슷한 형태로반복]
시나리오뿐만 아니라 어떤 데이터를 넣어야 하는지 예제도 제공해주고 있다.
// 지하철 노선 생성
// ----- Request -----
POST /lines HTTP/1.1
accept: */*
content-type: application/json; charset=UTF-8
{
"color": "bg-red-600",
"name": "신분당선"
}
// ----- Response -----
HTTP/1.1 201
Location: /lines/1
Content-Type: application/json
Date: Fri, 13 Nov 2020 00:11:51 GMT
{
"id": 1,
"name": "신분당선",
"color": "bg-red-600",
"createdDate": "2020-11-13T09:11:51.997",
"modifiedDate": "2020-11-13T09:11:51.997"
}
// 지하철 노선 목록 조회
...
인수테스트 작성은 기존 TDD의 실패 -> 구현 -> 리팩토링에서 앞에 한가지가 더 추가된 형태로 금방 익숙해진다.
인수테스트의 클라이언트 객체로 RestAssured를 사용한다. 아래는 인수테스트 예제 코드다.
@DisplayName("지하철역을 생성한다.")
@Test
void createStation() {
// given
Map<String, String> params = new HashMap<>();
params.put("name", "강남역");
// when
ExtractableResponse<Response> response = RestAssured.given().log().all()
.body(params)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.when()
.post("/stations")
.then().log().all()
.extract();
...
}
위와 같은 형식으로 시나리오를 인수테스트로 작성하고 기능을 구현하고 인수테스트가 통과되면 이번 미션은 종료다.
🚀 2단계 - 인수 테스트 리팩토링
요구사항
- 지하철역과 지하철 노선 이름 중복 금지 기능 추가
- 새로운 기능 추가하면서 인수 테스트 리팩터링
2단계는 이전 단계 코드에서 새로운 요구사항 반영과 리팩토링을 진행한다.
인수 테스트를 작성하다보면 클라이언트 객체와 도메인에서 중복 코드가 생성되는데 이를 분리한다.
class CommonRestAssured {
public static ExtractableResponse<Response> post(String url, Map params) {
return RestAssured.given().log().all()
.body(params)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.when()
.post(url)
.then().log().all()
.extract();
}
public static ExtractableResponse<Response> get(String url) {
return RestAssured.given().log().all()
.when()
.get(url)
.then().log().all()
.extract();
}
public static ExtractableResponse<Response> delete(String url) {
return RestAssured.given().log().all()
.contentType(MediaType.APPLICATION_JSON_VALUE)
.when()
.delete(url)
.then().log().all()
.extract();
}
public static ExtractableResponse<Response> put(String url, Map params) {
return RestAssured.given().log().all()
.body(params)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.when()
.put(url)
.then().log().all()
.extract();
}
}
이제 도메인별 인수테스트마다 중복되는 코드도 분리한다.
public class LineSteps {
private static final String URL = "/lines";
public static ExtractableResponse<Response> create(String name, String color, Long upStationId, Long downStationId, int distance) {
Map<String, Object> params = new HashMap<>();
...
return CommonRestAssured.create(URL, params);
}
public static ExtractableResponse<Response> get() {
return CommonRestAssured.get(URL);
}
public static ExtractableResponse<Response> get(Long lineId) {
return CommonRestAssured.get(URL + "/" + lineId);
}
public static ExtractableResponse<Response> delete(String url) {
return CommonRestAssured.delete(url);
}
public static ExtractableResponse<Response> modify(String url, String name, String color) {
Map<String, String> params = new HashMap<>();
params.put("name", name);
params.put("color", color);
return CommonRestAssured.modify(url, params);
}
}
그 외에도 @RestControllerAdvice, @ExceptionHandler 이용해서 에러 처리하면 마무리된다.
🚀 3단계 - 지하철 구간 관리
요구사항
- 지하철 노선 생성 시 필요한 인자 추가하기
- 지하철 노선에 구간을 등록하는 기능 구현
- 지하철 노선에 구간을 제거하는 기능 구현
- 지하철 노선에 등록된 구간을 통해 역 목록을 조회하는 기능 구현
- 구간 등록 / 제거 시 예외 케이스에 대한 인수 테스트 작성
1주차 마지막 단계로 이전까지 간단히 인수테스트에 적응되는 맛보기 단계였다면, 이번 단계부터 본격적인 ATDD를 진행한다.
요구사항을 기반으로 시나리오 작성하고 인수테스트, 기능을 구현하는데 아래 예시를 보면 TDD 3주차 사다리 타기를 떠올리게 만든다.
1주차를 진행하면서 큰 실수를 한가지 했다.
인수테스트도 테스트코드라는 생각에 단위테스트를 생략하고 기능을 구현했다가 에러가 발생했는데
디버깅하는데 오래 걸려서 뒤늦게 단위 테스트를 추가하면서 진행했다.
인수테스트, 통합테스트, 단위테스트가 생기면서 테스트를 잘 관리하기위해 중복을 제거하고 픽스쳐도 추가했지만
public class SectionFixData {
private static int DEFAULT_DISTANCE = 5;
public static String FIRST_STATION_NAME = "강남역";
public static String SECOND_STATION_NAME = "양재역";
public static String THIRD_STATION_NAME = "양재시민의숲";
public static String FOURTH_STATION_NAME = "판교역";
public static Line createLine() {
return new Line("2호선", "bg-green-700");
}
public static Station createStation(String name) {
return new Station(name);
}
public static Section createSection(Station upStation, Station downStation) {
return new Section(createLine(), upStation, downStation, DEFAULT_DISTANCE);
}
public static Sections createSections(Section ...sections) {
return new Sections(new ArrayList<>(Arrays.asList(sections)));
}
}
오히려 공통 의존이 되어 잘못변경하면 테스트 코드가 실패되고 파악하기 힘들어져서, 프로덕션 코드보다 시간이 소비되었다.
import nextstep.subway.exception.SectionException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
@DisplayName("구간들 관리")
public class SectionsTest extends SectionFixData {
@DisplayName("새로운 구간 생성할 때 노선의 마지막 하행역이 아닌 상행역이면 예외처리")
@Test
void matchDownStationException() {
// given
Station station1 = createStation(FIRST_STATION_NAME);
Station station2 = createStation(SECOND_STATION_NAME);
Station station3 = createStation(THIRD_STATION_NAME);
Station station4 = createStation(FOURTH_STATION_NAME);
...
}
...
}
테스트 코드를 보면 다양한 STATION_NAME 상수가 있는데 import 구문에도 없어서 어디에 있는지 파악하기 힘들다.
이런저런 이유로 피드백을 받아 수정하여 1주차는 마무리가 되었다.
📃 1주차 후기
1주차는 ATDD를 알아가는 시간이였고, TDD를 했던 사람이라면 금방 익숙해진다.
ATDD는 애자일 기법 중 하나로 인수테스트에 고객도 참여하기 때문에 TDD에서 선행 과정으로 실패하는 인수테스트가 있는 그림으로 표현한다.
단순히 TDD로 진행하면 요구사항으로 진행하다가 의문이 생겨 질문을 해야할 경우가 생기는데
ATDD는 그러한 사항을 최대한 해결하기 때문에 무엇을 구현할지 명확한 길을 제시해준다.
테스트 패키지를 보면 여러가지로 구성이 되어있다.
인수 테스트 패키지, 통합 테스트 패키지, 단위 테스트 패키지 그리고 유틸 패키지로 구성된 패키지는 테스트 코드 관리의 필요성을 느끼게 해준다.
테스트 패키지마다 중복되는 코드가 있다. 또한 다른 패키지에 있는 테스트와 중복되는 코드도 있다.
패키지를 뛰어넘는 중복을 제거하고 하나로 통합한다면 무분별한 의존성이 생기기 때문에 프로덕션 코드보다 변화에 민감한 코드를 맛볼 수 있다.
하지만 메서드 분리를 하면 테스트 코드 파악이 힘들 수도 있다.
무엇보다도 다른 사람이 테스트 코드르 볼 때 이해할 수 있도록 @DisplayName에 설명과 작명을 잘 해야한다.
정리할 내역
- 사용되는 도구 RestAssured 정리
- 효율적으로 테스트 코드 관리하는 방법 알아보기
'교육 및 인강 > ATDD, 클린 코드 with Spring' 카테고리의 다른 글
ATDD 5주차 - 마무리 (0) | 2022.07.25 |
---|---|
ATDD 4주차 - 테스트 기반 문서화 (0) | 2022.07.25 |
ATDD 3주차 - ATDD 기반 리팩토링 (0) | 2022.07.09 |
ATDD 2주차 - ATDD + TDD 후기 (0) | 2022.06.24 |
ATDD 시작하면서... (0) | 2022.06.09 |