본문 바로가기
가치관 쌓기/개발 돌아보기

[우아한테크코스Pro]인수테스트 주도 개발[3/9]

by simplify-len 2021. 6. 15.

인수테스트 주도 개발이란 무엇인가?

개인적인 생각으로서, 설명할 때테스트 주도 개발(TDD) 의 단점을 해소시켜주는 개발론 중 하나라고 말씀드리고 싶습니다.

TDD 라는 것은 실패한 테스트케이스를 작성하고, 이를 통과시키면서 개발해나가는 방법을 말합니다.

출처 - https://ichi.pro/ko/tdd-silijeu-1-bu-tdd-test-driven-development-lan-281005908595807

TDD 단점으로 생각해보면, 실패한 테스트라는 것을 무엇을 어디에서부터 어떻게 테스트를 해야될지? 판단하기 어렵다는 부분에 있습니다.

이런 문제를 해소시켜 주는 개발 방법론으로서 인수주도 테스트 개발 이라는 것이 나옵니다. 어떻게 이런 부분을 도와줄수 있을까요?

이부분을 학습하기 위해서 우아한테크캠프pro 에서는 3주차 과제로 인수 테스트 주도 개발을 체험해볼 수 있는 시간을 가졌습니다.

이미 어느정도 작성된 코드를 Fork 받아, 인수테스트 를 작성하고, 이를 Refactoring하는 것이 미션이였습니다. 조금더 구체적으로는 여러 도메인이 얽혀서 테스트하기 힘든 구조의 코드를 rest-assured 를 활용해 테스트를 먼저 작성 후 리팩토링하는 것입니다.

인수 주도 테스트 개발(ATDD) 프로세스에 대해서는, 본래 테스트를 목적으로 나온 것이 아니라고 합니다. 이는 애자일 실천 방법 중 하나로서, 다양한 관점을 가진 팀원들과 협업하기 위해서 나온 프로세스라고 합니다.

인수테스트란

  • 사용자 관점에서 올바르게 작동하는지 테스트
  • 인수 조건은 기술(혹은 개발)용어가 사용되지 않고(개발자가 아닌 )일반 사용자들이 이해할 수 있는 단어를 사용

클라이언트가 의뢰했던 소프트웨어를 인수 받을 때, 미리 전달했던 요구사항이 충족되었는지를 확인하는 테스트

인수 테스트의 특징으로

  • 전구간테스트
    • 요청과 응답 기준으로 전 구간을 검증
  • Black Box 테스트
    • 세부 구현에 영향을 받지 않게 구현

조금 더 자세한 내용은 아래 링크를 통해서 확인할 수 있습니다.

ATDD Cycle

Acceptance Test Driven Development

처음 인수주도테스트 에 대해서 이론적인 내용만 들어서는 사실 잘 와닿지 않았습니다. 역시나, 바로 코드를 보면서 인수주도테스트개발 이 무엇인지 이해해봅시다.

RestAssured 테스트 도구를 사용해 아래와 같은 코드를 작성합니다.

https://rest-assured.io/

아! 그 전에 무엇을 테스트할 것인가 라는 인수조건을 아래와같은 템플릿을 활용해 작업합니다.

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)을 사용하여 테스트

인수테스트 VS MockMvc

이번 우아한테크코스Pro 3주차를 진행하면서 알게 된 부분으로는

  • @DirtyContext
  • ATDD 사용시 어떻게 데이터베이스를 초기화 시킬수 있을까?
  • 도메인 주도의 개발시, getter 메소드를 주의깊게 사용할 것
  • @RequestMappingproducesconsumes 사용법
  • @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 에는 아래와 같은 설정 값이 있어서, 클래스와 메소드 그리고 상속타입에 적절한 값을 넣어 사용해야 합니다.

image-20210615192841008

더 자세한 예시

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 메소드를 주의깊게 사용할 것

@RequestMappingproducesconsumes 사용법

  • 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

 

[Step1] 지하철 노선 관리 by LenKIM · Pull Request #166 · next-step/atdd-subway-admin

안녕하세요. 리뷰어님😃 이번 우테캠Pro 에 참여하게된 김정규입니다. 이번 인수테스트 주도개발 미션 잘 부탁드리겠습니다. 마지막으로, 커밋을 나누어 작업하고자 의도했는데, REST-Assurd 를 체

github.com

https://github.com/next-step/atdd-subway-admin/pull/184

 

[Step2] 인수 테스트 리팩터링 by LenKIM · Pull Request #184 · next-step/atdd-subway-admin

안녕하세요. 리뷰어님! 2단계 리뷰 부탁드립니다. 코드를 잘 작성했는지 여부가 판단이 안되네요. 처음 고민했던 부분은 Section 을 어떤 기준으로 잡아야 하는지 고민이였습니다. JPA 를 학습했지

github.com

https://github.com/next-step/atdd-subway-admin/pull/227

 

[Step3] 구간 추가 기능 by LenKIM · Pull Request #227 · next-step/atdd-subway-admin

안녕하세요. 리뷰어님. 3단계 구간추가기능 구현이 굉장히 까다로웠습니다. 코드 리뷰 잘 부탁드립니다. 감사합니다.

github.com

https://github.com/next-step/atdd-subway-admin/pull/248#issuecomment-860236028

 

[Step4] 구간 제거 기능 by LenKIM · Pull Request #248 · next-step/atdd-subway-admin

벌써, 마지막 단계 이네요. 이번에도 코드 리뷰 잘부탁드립니다. 리뷰어님. 감사합니다.

github.com

댓글