안녕하세요 코딩하는헬린이입니다.
이번엔 ATDD 에 관하여 작성을 해볼려고 합니다.
근데 왜 TDD (테스트주도개발) 이 아닌 ATDD 에 관하여 포스팅 하냐면 이번에 ATDD 방법론 으로 개발을 한번 진행해봤기에 복습차 작성합니당.
ATDD (Acceptance Test Driven Development) 란 ?
직역하면 인수 테스트 주도 개발로,
ATDD ( 인수 테스트 주도 개발 )은 비즈니스 고객, 개발자 및 테스터 간의 커뮤니케이션을 기반으로 하는 개발 방법론입니다.
쉽게 팀원이 모든 요구사항을 체크하고 사전에 이해한 바탕으로 개발을 진행하는 개발론입니다.
아니 그럼 요구사항을 알고 개발을 진행해야지 ? 아니면 어떻게 진행을해? 라고 생각하실수 있습니다.
근데 아래의 내용을 보시면 다른게 보이실수 있습니다.
일단 해당 내용을 알기위해 표준적으로 알려진 TDD 싸이클에 대하여 간략하게 보자면
- 테스트를 먼저 작성하고 결과가 fail인 것을 확인.
- 작성한 테스트를 통과할 정도 만큼의 코드를 작성
- 동작(behavior)를 변경하지 않고 코드를 개선
위와 같은 싸이클로 진행되는 TDD에 반면
ATDD 의 사이클은 아래와 같습니다.
- 개발 이전에 협업하여 인수조건에 관하여 User story 에 관한 요구사항 작성
- User story 기반의 테스트 코드 작성 (개발이 진행되기전 작성)
- 요구사항 기준으로 테스트 코드 작성이 완료 되었다면 개발 진행 (프로덕션 코드 작성)
ATDD는 개발을 하기전 테스트 코드부터 작성하는 구조로 보실수 있습니다.
ATDD 목적 ?
- ATDD 로 인하여 TDD 극대화
- 요구사항을 먼저 작성한후 개발을 집중하여 클라이언트가 원하는 결과물에 흡사하게 개발진행
- 개발할 기능에 대하여 높은 이해도
- 전체적인 프로세스를 테스트 코드로 가독성 증가
인수 조건 정의
-
- 인수 조건 작성 방법
- 검증하고자 하는 when 구문 먼저 작성
- 기대 결과를 의미하는 then 구문 작성
- when과 then에 필요한 정보를 given을 통해 작성
- 인수 조건 작성 표현인 given/when/then 테스트 코드는 아래와 같다
- 인수 조건 작성 방법
@DisplayName("지하철 구간 관리 기능")
class SectionAcceptanceTest extends AcceptanceTest {
private Long 신분당선;
private Long 강남역;
private Long 양재역;
/**
* Given 지하철역과 노선 생성을 요청 하고
*/
@BeforeEach
public void setUp() {
super.setUp();
강남역 = 지하철역_생성_요청("강남역").jsonPath().getLong("id");
양재역 = 지하철역_생성_요청("양재역").jsonPath().getLong("id");
Map<String, String> lineCreateParams = createLineCreateParams(강남역, 양재역);
신분당선 = 지하철_노선_생성_요청(lineCreateParams).jsonPath().getLong("id");
}
/**
* When 지하철 노선에 새로운 구간 추가를 요청 하면
* Then 노선에 새로운 구간이 추가된다
*/
@DisplayName("지하철 노선에 구간을 등록")
@Test
void addLineSection() {
// when
Long 정자역 = 지하철역_생성_요청("정자역").jsonPath().getLong("id");
지하철_노선에_지하철_구간_생성_요청(신분당선, createSectionCreateParams(양재역, 정자역));
// then
ExtractableResponse<Response> response = 지하철_노선_조회_요청(신분당선);
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value());
assertThat(response.jsonPath().getList("stations.id", Long.class)).containsExactly(강남역, 양재역, 정자역);
}
/**
* Given 지하철 노선에 새로운 구간 추가를 요청 하고
* When 지하철 노선의 마지막 구간 제거를 요청 하면
* Then 노선에 구간이 제거된다
*/
@DisplayName("지하철 노선에 구간을 제거")
@Test
void removeLineSection() {
// given
Long 정자역 = 지하철역_생성_요청("정자역").jsonPath().getLong("id");
지하철_노선에_지하철_구간_생성_요청(신분당선, createSectionCreateParams(양재역, 정자역));
// when
지하철_노선에_지하철_구간_제거_요청(신분당선, 정자역);
// then
ExtractableResponse<Response> response = 지하철_노선_조회_요청(신분당선);
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value());
assertThat(response.jsonPath().getList("stations.id", Long.class)).containsExactly(강남역, 양재역);
}
private Map<String, String> createLineCreateParams(Long upStationId, Long downStationId) {
Map<String, String> lineCreateParams;
lineCreateParams = new HashMap<>();
lineCreateParams.put("name", "신분당선");
lineCreateParams.put("color", "bg-red-600");
lineCreateParams.put("upStationId", upStationId + "");
lineCreateParams.put("downStationId", downStationId + "");
lineCreateParams.put("distance", 10 + "");
return lineCreateParams;
}
private Map<String, String> createSectionCreateParams(Long upStationId, Long downStationId) {
Map<String, String> params = new HashMap<>();
params.put("upStationId", upStationId + "");
params.put("downStationId", downStationId + "");
params.put("distance", 6 + "");
return params;
}
}
위와 같이 인수 조건 테스트 코드를 먼저 작성한 후
API 에 관한 전체적인 프로세스를 작성하시면 됩니다.
Restassured 라는 라이브러리를 사용하시면 됩니다.
@DisplayName("지하철역 관련 기능")
public class StationAcceptanceTest extends AcceptanceTest {
/**
* When 지하철역을 생성하면
* Then 지하철역이 생성된다
* Then 지하철역 목록 조회 시 생성한 역을 찾을 수 있다
*/
@DisplayName("지하철역을 생성한다.")
@Test
void createStation() {
// when
ExtractableResponse<Response> response = 지하철역_생성_요청("강남역");
// then
assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value());
// then
List<String> stationNames =
RestAssured.given().log().all()
.when().get("/stations")
.then().log().all()
.extract().jsonPath().getList("name", String.class);
assertThat(stationNames).containsAnyOf("강남역");
}
/**
* Given 2개의 지하철역을 생성하고
* When 지하철역 목록을 조회하면
* Then 2개의 지하철역을 응답 받는다
*/
@DisplayName("지하철역을 조회한다.")
@Test
void getStations() {
// given
지하철역_생성_요청("강남역");
지하철역_생성_요청("역삼역");
// when
ExtractableResponse<Response> stationResponse = RestAssured.given().log().all()
.when().get("/stations")
.then().log().all()
.extract();
// then
List<StationResponse> stations = stationResponse.jsonPath().getList(".", StationResponse.class);
assertThat(stations).hasSize(2);
}
/**
* Given 지하철역을 생성하고
* When 그 지하철역을 삭제하면
* Then 그 지하철역 목록 조회 시 생성한 역을 찾을 수 없다
*/
@DisplayName("지하철역을 제거한다.")
@Test
void deleteStation() {
// given
ExtractableResponse<Response> createResponse = 지하철역_생성_요청("강남역");
// when
String location = createResponse.header("location");
RestAssured.given().log().all()
.when()
.delete(location)
.then().log().all()
.extract();
// then
List<String> stationNames =
RestAssured.given().log().all()
.when().get("/stations")
.then().log().all()
.extract().jsonPath().getList("name", String.class);
assertThat(stationNames).doesNotContain("강남역");
}
}
물론 이렇게 작성을 하였다면 처음에는 비니지스 로직이없어 오류가 나겠지만..
앞으로 개발할때 먼저 작성한 테스트 코드로 인하여 비지니스 로직을 개발할때 보다 더 TDD의 강점을 극대화 시킬수 있는듯 하며
보다 프로덕션 코드를 안전하게 개발할 수 있는 여건이 마련되는듯 합니다.
ref
https://github.com/next-step/atdd-subway-path
https://velog.io/@windtrip/ATDDAcceptance-Test-Driven-Development