728x90
ATDD
TDD는 왜 어려운가
- 실무에 적용하기에는 일정 수준의 숙련도가 필요
- 테스트 툴에 익숙하지 않다 보니 어느 순간부터 테스트를 작성하는 공수가 더 들때가 있다
- 그러다 보니 TDD 흐름을 관리하기가 힘들다
- 레거시 테스트를 지속적으로 관리를 해줘야 하는데 어느 순간부터 손댈 수 없게 되어버린다
- 테스트의 목적에 포커스를 두기보다는 학문적 목표라는 느낌이 강하다
- 도메인부터 단위테스트를 통해 기능을 구현하려 한다
- 도메인 로직을 모르는 상태에서 시작하면 막막해져버린다
- 도메인 관계 설계가 어려워서 지우고 생성하다보면 어느순간부터는 잊어버리게 된다
- 막상 기능 통합을 하면 삐그덕 거린다
- 서비스가 국한된 부분을 강조하다 보니 전체를 놓치게된다
ATDD
- 인수테스트를 먼저 구현하고 단위테스트를 구현한다!
- 인수테스트 기반으로 TDD 가능
- 무엇을 테스트 해야되는가에 대한 고민을 해결해줄 수 있다
- 전체적인 흐름을 잃지 않을 수 있다
- 단위 테스트의 목적을 분명히 할 수 있는 것 같다
- 레거시 코드에서도 TDD 가능
ATDD 사이클
- 인수테스트 먼저 작성
- 하위 단위 테스트 작성
- 인수테스트 종료시, 기능 구현 종료
상세 ATDD 사이클
- 요구사항 정리
- 문서화
- 인수테스트 작성
- 하위 단위테스트 작성
- API 개발
- 테스트 리펙터링
ATDD 코딩 순서
- 인수테스트 작성
- 인수테스트 성공
- 컨트롤러 작성
- 단위 테스트 작성
- 컨트롤러 테스트
- 컨트롤러 테스트 상세 작성
- 서비스 추가
- 컨트롤러 구현
- 서비스 테스트
- 서비스 테스트 상세 작성
- 서비스 구현
만들어져야 될 객체들을 Mock으로 대체한 후 테스트
- 테스트 하는 대상이 관계를 맺고 있다면 mock객체로 대체 후 테스트
- 관계를 맺는 클래스가 정의 안되있다면 새로운 클래스 생성
- OutsideIn
OutsideIn
- 인터페이스를 만들면서 테스트를 작성
- 만들어져야될 협력 객체들을 예측하여 대상과 협렵객체의 상호작용 고려
- 테스트 성공 이후 mock객체에 대한 명세를 시작
- 다음 테스트의 시작점
ATDD 리펙토링
- 테스트의 의도를 명확히 드러내기
// given
지하철_노선_등록되어_있음
// when
지하철_노선_조회_요청
// then
지하철_노선_응답됨
- 테스트 코드 중복제거
- 가독성이 좋지 못한 테스트 코드는 관리가 안된다
- 테스트 코드도 중복이 많이 발생한다
- 테스트의 가독성을 높힌다
- 중복제거 방법
- 메서드 분리
- CRUD 추상화
- Cucumber나 JBehave 같은 BDD 도구 사용
테스트 픽스쳐
- 테스트 대상에 필요한 모든 것
- SUT : 테스트할 대상 (System Under Test)
- 그 중 공유되는 자원의 관리가 중요하다
- 테스트는 독립되어야 한다
- 트스트의 순서는 보장되지 못한다
- 테스트간 공유되는 자원 관리가 중요하다
- 테스트간 공유되는 자원
- Spring Context
- Database
- 픽스처 관리
- 매 테스트 수행 전 공유되는 자원 초기화
- 매 테스트 수행 전 클라이언트의 request 요청으로 설정
- 이때 발생하는 중복 코드는
@BeforeEach
메소드로 중복제거
테스트 시나리오 관리
- 위로 갈 수록 추상적
- Business Rule 단계에서 인수테스트 시나리오를 작성하는 것이 좋을 것 같다
- Techincal Activity에서 단위테스트의 향수가 느껴진다
- 매우 구체적으로 작성
- Technical Activity
- 기술과 관련된 내용으로 시나리오 작성
- 기술이 바뀌면 테스트 시나리오 수정
1. 최단 경로를 조회할 수 있는 페이지로 접속한다.
2. 조회하고자 하는 경로의 출발지와 도착지를 각각 "강남역"과 "잠실역"으로 **입력**한다.
3. **조회하기 버튼**을 누른다.
4. 결과 페이지에서 출발지와 도착지 사이의 지하철역 목록을 확인한다.
- Work Flow
- 작업 순서를 바탕으로 시나리오 작성
- 작업 순서가 바뀌면 테스트 시나리오 수정
- 기술이 바껴도 수정할 필요없다
1. 최단 경로를 조회할 수 있는 페이지로 접속한다.
2. 조회하고자 하는 경로의 출발지와 도착지를 입력한다.
3. 조회한다.
4. 출발역과 도착역 사이의 지하철 역 목록을 확인한다.
- Business Rule
- 비즈니스 규칙에 따라 시나리오 작성
- 작업 순서, 기술이 바껴도 수정하지 않는다
1. Scenario: 출발역과 도착역 사이의 최단 경로를 조회
2. When: 출발역과 도착역을 기준으로 최단 경로를 조회한다.
3. Then: 출발역과 도착역 사이의 지하철 역을 확인할 수 있다.
테스트 비용
- 모든 것을 인수테스트로 검증하지 않는다
- 반드시 필요한 인수조건에 대해 인수테스트로 구현
- 예외 검증, 사이드 케이스의 경우 단위 테스트로 검증
- 필요한 인수테스트만 하고 구체적 케이스는 단위테스트로
레거시 리펙토링
- 단위 테스트
- 레거시 코드에 단위테스트 작성
- 테스트가 깨지지 않은 상태로 리펙토링
- 도메인 지식이 있을 경우 유용
- 객체 설계의 어려움 → TDD 사이클 진행이 안됨
- 도메인이해 부족으로 단위테스트 작성이 어려움
- 인수 테스트
- 레거시 코드에 인수테스트 작성
- 테스트가 깨지지 않은 상태로 리펙토링
- 제일 밖부터 단위테스트를 추가하면서 리펙터링
- 사용자 입장에서 검증하는 인수테스트를 보너스로 추가
- 도메인 부족 시 유용
@DisplayName("지하철 노선에 등록된 마지막 지하철역을 제외한다.")
@Test
void removeLineStation1() {
// given
Long lineId = 지하철_노선_등록되어_있음("2호선", "GREEN");
Long stationId1 = 지하철역_등록되어_있음("강남역");
Long stationId2 = 지하철역_등록되어_있음("역삼역");
Long stationId3 = 지하철역_등록되어_있음("선릉역");
지하철_노선에_지하철역_등록되어_있음(lineId, null, stationId1);
지하철_노선에_지하철역_등록되어_있음(lineId, stationId1, stationId2);
지하철_노선에_지하철역_등록되어_있음(lineId, stationId2, stationId3);
// when
ExtractableResponse<Response> deleteResponse = 지하철_노선에_지하철역_제외_요청(lineId, stationId3);
// then
지하철_노선에_지하철역_제외됨(deleteResponse);
// when
ExtractableResponse<Response> response = 지하철_노선_조회_요청(lineId);
// then
지하철_노선에_지하철역_제외_확인됨(response, stationId3);
지하철_노선에_지하철역_순서_정렬됨(response, Arrays.asList(stationId1, stationId2));
}
단위테스트와 통합테스트
단위테스트
- 단위는 단일 기능
- 단위는 메소드에서 클래스까지 다양
- 테스트 단위가 통합인지 고립인지 먼저 고려
- 통합 : 협력 객체가 있는 경우
- 고립 : 협력 객체가 없는 경우
- 자원이 안정적이라면 굳이 mock으로 고립시킬 필요없이 mockBean 등의 대역 사용도 괜찮다
Test Double
- 실제 객체 대신 사용되는 모든 객체
Stubbing
- 호출되면 미리 지정된 답변으로 응답
mockito
public class MockitoTest {
@Test
void findAllLines() {
...
LineRepository lineRepository = mock(LineRepository.class);
StationRepository stationRepository = mock(StationRepository.class);
when(lineRepository.findAll()).thenReturn(Lists.newArrayList(new Line()));
...
}
}
mockitoExtension
@ExtendWith(MockitoExtension.class)
public class MockitoExtensionTest {
@Mock
private LineRepository lineRepository;
@Mock
private StationRepository stationRepository;
@Test
void findAllLines() {
...
when(lineRepository.findAll()).thenReturn(Lists.newArrayList(new Line()));
...
}
}
springExtension
@ExtendWith(SpringExtension.class)
public class SpringExtensionTest {
@MockBean
private LineRepository lineRepository;
@MockBean
private StationRepository stationRepository;
@Test
void findAllLines() {
...
when(lineRepository.findAll()).thenReturn(Lists.newArrayList(new Line()));
...
}
}
통합 테스트
- 데이터베이스, 파일 시스템, 외부 라이브러리 등 다른 어플리케이션과 통합되어 개발하는 테스트
- 외부 라이브러리
- 외부 라이브러리 기능에 대해 검증할 필요는 없다
- 단, 사용하는 부분에 대한 검증은 필요
- 실제 객체 사용한다
- 외부 라이브러리를 심도있게 이해하기 어려움
- 목 객체와 실제 객체의 행동을 일치하기 어려움
- 데이터 베이스
- 테스트용 메모리 DB에 연결 사용
통합 테스트 vs 단위 테스트
- 통합 테스트
- 실제와 가까운 환경에서 검증
- 정상적 기능 작동 여부
- 단위 테스트
- 통합 부분이 정상적으로 동작한다고 가정
- 단일 기능만 검증
- 결국, 목적과 상황에 맞게 테스트해라
API 설계
- 요청 시,
url
로 리소스 지정 (ex. www.google.com) - 요청 시,
method
로 행위 결정 (ex. GET) - 요청 시,
accept header
로 응답 리소스 표현 결정 (ex. application/json) - 요청 시,
content-type header
로 보낼 데이터 형식 알림 (ex. application/json) - 응답 시,
status code
로 응답 결과 요약
Resource
- 네트워크에서 관리하는 모든 정보
- 문서, 이미지 등
- 정확히는 Representation
- 네트워크를 대표하는 정보라는 뜻인듯
request
// start-line
GET /stations/1 HTTP/1.1
// header
accecpt: application/json
host: localhost:57906
connection: Keep-Alive
user-agent: Apache-HttpClient/4.5.12 (Java/1.8.0_252)
accept-encoding: gzip,deflate
response
// start-line
HTTP/1.1 200
// header
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 10 Jul 2020 09:38:40 GMT
Keep-Alive: timeout=60
Connection: keep-alive
// message body
{
"createdDate": "2020-07-10T18:38:40.549",
"modifiedDate": "2020-07-10T18:38:40.549",
"id": 1,
"name": "강남역"
}
Resource Naming
- 리소스에 접근할 URI 중 path 부분
- ex. http://localhost:8080/station/1
Path
- 계층 구조를 가진다
- scheme (required)
- authority
- path (required)
- query
- fragment
- authority가 주어진다면 path는 공백이거나 /이어야 한다
- authority가 없다진다면 path는 //가 아니여야 한다
- https://tools.ietf.org/html/rfc3986#section-3
path 형식
- 복수 리소스 (ex.
/customers/
) - 단수 리소스 (ex.
/customers/{id}
) - 하위 리소스 (ex.
/customers/{id}/account
)
Resource 종류별 표현
- document
- 단수개의 리소스 표현
/customers/{id}
- collection
- 복수개의 리소스 표현
/customers
- store
- 클라이언트에서 관리하는 리소스
- document와 다르게 고유 식별자 없음
- 복수로 표현
/customers/{id}/account
- controller
- 절차라는 리소스
- 실행가능한 함수와 유사
- 매개변수와 반환값이 존재
- 동사를 사용해도 좋다
/resources/{id}/checkout
API 설계 문서 참고
728x90