인수테스트 주도 개발이란 무엇인가?
개인적인 생각으로서, 설명할 때테스트 주도 개발(TDD)
의 단점을 해소시켜주는 개발론 중 하나라고 말씀드리고 싶습니다.
TDD
라는 것은 실패한 테스트케이스를 작성하고, 이를 통과시키면서 개발해나가는 방법을 말합니다.
TDD
단점으로 생각해보면, 실패한 테스트라는 것을 무엇을 어디에서부터 어떻게 테스트를 해야될지? 판단하기 어렵다는 부분에 있습니다.
이런 문제를 해소시켜 주는 개발 방법론으로서 인수주도 테스트 개발
이라는 것이 나옵니다. 어떻게 이런 부분을 도와줄수 있을까요?
이부분을 학습하기 위해서 우아한테크캠프pro
에서는 3주차 과제로 인수 테스트 주도 개발
을 체험해볼 수 있는 시간을 가졌습니다.
이미 어느정도 작성된 코드를 Fork 받아, 인수테스트
를 작성하고, 이를 Refactoring
하는 것이 미션이였습니다. 조금더 구체적으로는 여러 도메인이 얽혀서 테스트하기 힘든 구조의 코드를 rest-assured 를 활용해 테스트를 먼저 작성 후 리팩토링하는 것입니다.
인수 주도 테스트 개발(ATDD)
프로세스에 대해서는, 본래 테스트를 목적으로 나온 것이 아니라고 합니다. 이는 애자일 실천 방법 중 하나로서, 다양한 관점을 가진 팀원들과 협업하기 위해서 나온 프로세스라고 합니다.
인수테스트란
- 사용자 관점에서 올바르게 작동하는지 테스트
- 인수 조건은 기술(혹은 개발)용어가 사용되지 않고(개발자가 아닌 )일반 사용자들이 이해할 수 있는 단어를 사용
클라이언트가 의뢰했던 소프트웨어를 인수 받을 때, 미리 전달했던 요구사항이 충족되었는지를 확인하는 테스트
인수 테스트의 특징으로
- 전구간테스트
- 요청과 응답 기준으로 전 구간을 검증
- Black Box 테스트
- 세부 구현에 영향을 받지 않게 구현
조금 더 자세한 내용은 아래 링크를 통해서 확인할 수 있습니다.
ATDD Cycle
처음 인수주도테스트
에 대해서 이론적인 내용만 들어서는 사실 잘 와닿지 않았습니다. 역시나, 바로 코드를 보면서 인수주도테스트개발
이 무엇인지 이해해봅시다.
RestAssured
테스트 도구를 사용해 아래와 같은 코드를 작성합니다.
아! 그 전에 무엇을 테스트할 것인가 라는 인수조건을 아래와같은 템플릿을 활용해 작업합니다.
Feature: 간략한 기능 서술
Background: 각 시나리오 사전 조건
Scenario: 시나리오(예시) 제목
Given: 사전조건
When: 발생해야하는 이벤트
Then: 사후조건
And: 앞선 내용에 추가적인 내용 기술
Feature: 지하철 역 관리 기능
Scenario: 지하철 역을 생성한다.
When 지하철 역을 생성 요청한다.
Then 지하철역이 생성된다.
Scenario: 지하철 역을 삭제한다.
Given 지하철 역이 등록되어있다.
When 지하철 역을 삭제 요청한다.
Then 지하철 역이 삭제된다.
@DisplayName("지하철역 관련 기능")
class StationAcceptanceTest extends AcceptanceTest {
@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();
// then
assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value());
assertThat(response.header("Location")).isNotBlank();
}
...
행위자체에 초점을 맞쳐보면, 위 코드는 실제 사용자 관점에서 지하철역을 생성하려고 할 때 발생하는 REST API와 동일합니다.
위에서 언급된 Block Box 테스트
라고 다시한번 생각해보자. API를 사용하는 고객 입장에서는 Spring Code 가 어떻게 작성되어있는지 알 필요가 없고, 알아서도 안됩니다. 즉 BlockBox
여야 한다는 관점입니다. 위 코드를 살펴보면 Spring 과 관련된 코드가 있나요? 없습니다. 즉, 스프링과 관련된 어떠한 요소 없이 오로지, 클라이언트 관점에서만 테스트 하는 것과 동일합니다.
PostMan
으로 API 요청하는 것과 크게 다르지 않지만, 다른 점이 있다면- 응답값을 코드로서 검증하고 이를 개발자에게 알려줄수 있다는 점입니다.
또한 MockMvc
를 생각해보면 조금더 인수테스트주도개발
에 대해서 이해할 수 있습니다. @Autowire
와 같은 스프링 컴포넌트를 하나도 사용하지 않고 테스트합니다.
관련해서 내용을 정리할까 했는데, 역시나 인터넷에 비교글이 있더군요. 아래 주소 공유합니다.
MockMvc vs WebTestClient vs RestAssured
- MockMvc는 @SpringBootTest의 webEnvironment.MOCK과 함께 사용 가능하며 mocking 된 web environment(ex tomcat) 환경에서 테스트
- WebTestClient는 @SpringBootTest의 webEnvironment.RANDOM_PORT 나 DEFINED_PORT와 함께 사용, Netty를 기본으로 사용
- RestAssured는 실제 web environment(Apache Tomcat)을 사용하여 테스트
이번 우아한테크코스Pro
3주차를 진행하면서 알게 된 부분으로는
@DirtyContext
ATDD 사용시 어떻게 데이터베이스를 초기화 시킬수 있을까?
- 도메인 주도의 개발시,
getter
메소드를 주의깊게 사용할 것 @RequestMapping
시produces
와consumes
사용법@RestControllerAdvice(annotations = RestController.class)
@DirtyContext
@Component
public class BranchFakeRepository {
private final List<Branch> branchList = new ArrayList<>();
public void save(Branch branch){
branchList.add(branch);
}
public List<Branch> findAll(){
return branchList;
}
public void print(){
System.out.println(branchList);
}
}
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = AppConfig.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
class DirtyContextTest {
@Autowired
BranchFakeRepository branchRepository;
@Test
void create() {
branchRepository.save(new Branch("A"));
assertThat(branchRepository.findAll()).hasSize(1);
}
@Test
void create2() {
branchRepository.save(new Branch("A"));
assertThat(branchRepository.findAll()).hasSize(1);
}
@Test
void create3() {
branchRepository.save(new Branch("A"));
assertThat(branchRepository.findAll()).hasSize(1);
}
}
@DirtiesContext
Context 가 더렵해지는 것을 막기위해서 사용합니다. 해당 어노테이션이 있을 경우, 이전에 더렵해진 Context 를 지우는 효과가 있습니다.
만약 @DirtiesContext
가 없으면 첫번째 테스틑 제외한 모든 테스트는 Fail 됩니다. 또한 @DirtiesContext
에는 아래와 같은 설정 값이 있어서, 클래스와 메소드 그리고 상속타입에 적절한 값을 넣어 사용해야 합니다.
ATDD 사용시 어떻게 데이터베이스를 초기화 시킬수 있을까?
바로 위 고민와 비슷한데, 인수테스트시에는 위 @DirtiesContext
가 동작하지 않습니다. 왜냐하면 @DirtiesContext
는 Spring-test에서 사용되는 주석이기 때문입니다. 그렇다면 ATDD 에서는 @DirtiesContext
와 비슷한 효과를 어떻게 줄수 있을까요?
대표적인 예시로 @GenerateValue()
와 같이 숫자 1,2,3 이 올라가는 것은 어떻게 초기화 시킬수 있을까? 이 문제에 대해서 넥스트스텝의 브라운은 코드로서 해결합니다.
다음과 같은 인수테스트를 위한 코드를 만들어, 하나의 클래스가 마칠때마다 Context 를 초기화 시키는 방식을 사용합니다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class AcceptanceTest {
@LocalServerPort
int port;
@Autowired
private DatabaseCleanup databaseCleanup;
@BeforeEach
public void setUp() {
RestAssured.port = port;
databaseCleanup.execute();
}
}
사실 @SpringBootTest
이 붙어서 여러 테스트를 할 때 시간적인 문제가 발생할 수 있는 요소는 여전히 남아있습니다.
도메인 주도의 개발시, getter
메소드를 주의깊게 사용할 것
@RequestMapping
시 produces
와 consumes
사용법
- produces 는 accept 헤더의 값에 따라 매핑 여부를 판단
- consumes는 content-type 헤더의 값에 따라 매핑 여부를 판단
@GetMapping(value = "/stations", produces = MediaType.APPLICATION_JSON_VALUE)
@RestControllerAdvice(annotations = RestController.class)
Spring 에서 예외를 처리하는 방법에 대해서 사용해본적이 없었는데, 간단히 도구 사용법 정도로 익혔습니다.
원래는 XXXController.java
에 붙어있던 부분의 예외를 이를 위 어노테이션을 활용해 한 곳으로 에러를 모일수 있도록 도와주는 정도로 이해했습니다.
Pull Request
https://github.com/next-step/atdd-subway-admin/pull/166
https://github.com/next-step/atdd-subway-admin/pull/184
https://github.com/next-step/atdd-subway-admin/pull/227
https://github.com/next-step/atdd-subway-admin/pull/248#issuecomment-860236028
'가치관 쌓기 > 개발 돌아보기' 카테고리의 다른 글
[우아한테크코스Pro] 그럴듯한 서비스 만들기 - 2 [4/9] (0) | 2021.07.01 |
---|---|
[우아한테크코스Pro] 그럴듯한 서비스 만들기(network) - 1 [4/9] (0) | 2021.06.17 |
[우아한테크코스Pro]QnA 서비스(JPA)[2/9] (2) | 2021.06.06 |
[우아한테크코스Pro]로또 구현(테스트 주도 개발) - 못다한 이야기 (0) | 2021.06.03 |
[우아한테크코스Pro]로또 구현(테스트 주도 개발)[1/9] (1) | 2021.05.28 |
댓글