Skip to content

Latest commit

 

History

History
291 lines (195 loc) · 28.9 KB

[Test] 단위 테스트의 모호함과 다양한 SW 테스트에 대하여.md

File metadata and controls

291 lines (195 loc) · 28.9 KB

단위 테스트의 모호함과 다양한 SW 테스트에 대하여

소프트웨어에 테스트는 왜 필요한걸까?

궁극적인 목표는 "지속 가능한 소프트웨어"를 만들기 위해서이다. 구글 엔지니어는 이렇게 일한다 라는 책에 따르면, "테스트는 엔지니어에게 신뢰를 줄 때만 가치가 있다."라고 했고, "레거시 코드 활용 전략"이라는 책에서는 "테스트 코드 루틴이 없는 코드"를 레거시 코드라고 정의했다.
그만큼 테스트는 중요하다. 테스트를 통해 기대하는 것은 신뢰성 있는 프로그램이다 - 회귀 방지와 정상적으로 동작할 것이라는 믿음이다.

소프트웨어는 개발 과정에서 새로운 기능의 추가와 리팩토링이 계속해서 일어난다. 개발 과정에서 신경을 쓰더라도 복잡도가 높아질 수 밖에 없고, 새로운 기능이나 기존 기능의 변경은 계속 있기 마련이다.
그럴 때마다 자동화된 SW 테스트가 없다면 어떻게 될까? 개발자들은 개발마다 벌벌 떨어야 하고, 지루하고 비효율적이고 실수하기 쉬운 방식으로 직접 테스트 해봐야 한다. 소프트웨어 회귀가 무서워 포스트맨을 키며 요청을 날려보게 된다. 그러다가 실수로 검증을 빼먹거나, 바빠서 넘어가게 되면 서비스 장애로 이어질 수도 있다.

이러한 비효율을 SW 자동화 테스트로 해결할 수 있다. 테스트를 통해 변경 이후에도 우리 소프트웨어가 어느 정도는 잘 작동할 것이라고 확신할 수 있고, 개발 조직의 생산성이 높아진다.
그리고 개발 조직의 생산성은 비즈니스의 신속함으로 이어지기 때문에 소프트웨어 기업에서 테스트는 중요한 위치에 있다.

1. 단위테스트는 무엇이고 왜 필요한걸까?

1.1 단위테스트 그 모호함

단위 테스트란 무엇인가?
Wiki 정의에 따르면,

마틴 파울러의 유명한 아티클 Unit Test에 따르면, 그가 한 테스트 전문가에게 "Unit Test란 무엇입니까?" 라고 물었을 때, "in the first morning of my training course I cover 24 different definitions of unit test" 라고 답했다고 한다. 단위 테스트에 대한 정의가 너무나도 다양하다. 이러한 모호함 때문에 뒤에 소개할 책 "구글 엔지니어는 이렇게 일한다"에서는 테스트의 분류를 소형-중형-대형의 "크기"로 분류하기도 한다.

일단 마틴 파울러의 아티클에 따르면, 다양한 Unit Test의 정의에도 몇 가지 공통점을 추려낼 수 있었다고 한다.

  1. Unit Test는 소프트웨어 시스템의 작은 부분에 집중한다.
  2. 요즘의 Unit Test는 프로그래머가 자신들의 "regular tools"을 통해 작성하려고 한다. (아마도 사용중인 언어를 말하는 것 같다.)
    유일한 차이점은 어떤 Unit Testing Framwork를 사용하냐이다.
  3. Unit Test는 다른 종류의 테스트보다 훨씬 빠르게 실행되어야 한다.



차이점은 "Unit의 정의"이다.
무엇이 "Unit"인가? 하나의 클래스인가, 단일 함수인가, 하나의 "기능"인가?
(마틴 파울러는 밀접하게 관계를 가진 여러 클래스나 클래스 내의 일부 메서드를 Unit이라고 간주함)


결국 공통적으로 검증하려는 범위가 작고, 실행 속도가 빠르다는 특징이 있었다. 그리고 차이로는 "Unit"을 어떻게 정의하느냐였다.
이 "무엇을 검증할 것이냐"의 정의에서 모호함이 온다. 결국 기준은 모두 다를 수 있기 때문이다.
중요한 것은 팀에서 어떻게 정의하는지가 될 것이다. 의존성을 끊고 단 하나의 클래스, 메서드만 검증할 것인가? 밀접한 객체의 의존성까지 포함할 것인가. 이에 관한 마틴 파울러의 생각은 "Solitary or Sociable?" 부분에서 확인해보자. -> Unit Test - martin fowler

다만, "구글 엔지니어는 이렇게 일한다"에 따르면 검증할 범위를 어떻게 잡든 간에, 단위 테스트는 상호작용이 아니라 상태를 테스트해야 한다고 한다.
그러니까, Service의 한 메서드를 테스트 하는 과정에서 Persistance Layer의 상호작용은 빠지는 것이 좋다고 말한다.
왜냐하면 대체로 상호작용이 포함된 테스트는 상태 테스트보다 깨지기 쉽다! "실제로 버그가 없음에도 상관없는 변경 때문에 실패하는 테스트"는

  1. 테스트 자체에 대한 신뢰도를 낮춘다.
  2. 주요 기능을 구현하고 테스트하는 과정에서, 관련 없는 테스트 수정 때문에 시간을 낭비하게 된다.

상태 테스트는 결과가 “무엇”인지 - "What"에 집중하고,
상호작용 테스트는 시스템이 “어떻게" - "How" 작동하는지를 테스트 하는 것이 목적이다!

단위 테스트의 목적은 대체로 상호작용 테스트와는 거리가 멀다. 따라서 팀에서 "Unit"의 범위를 어떻게 정의하든 간에 "상호작용 테스트"의 맥락이 섞이지 않도록 주의해야 할 것이다.
또한 행위를 테스트 하지 말라는 이야기로 오해하지는 말자. 메서드를 테스트 하는 경우 메서드의 행위를 테스트 하는 것은 당연하다. 이때 (가능하면) 상태의 변화로 결과를 확인하면 되므로, 행위를 테스트하지 말라는 말은 아니다.
오히려 테스트 코드의 구조나 설명은 "행위"가 중심이 되어야 한다.

이 파트는 좋은 단위 테스트에 대한 설명을 하기 위한 파트는 아니므로, 좋은 단위 테스트에 대한 설명은 후술하겠다.

  • 결론 : 단위 테스트는 여러 정의가 있고 모호하지만, 결국 작고 빠르다는 공통점이 있으며, 행위와 상태를 테스트 해야 한다.

1.2 작고 빠르다는 것의 장점

결국 단위 테스트는 공통적으로 검증할 범위가 작고, 실행 속도가 빠르다. 이러한 특징 덕분에 여러 장점을 가질 수 있다.

1. 빠르고 결정적이어서 개발자들이 수시로 수행하며 피드백을 즉각 얻을 수 있다.

프로그램을 구현하고 관리하는 과정에서 코드는 하루에 몇 번이고 몇 번이고 바뀌게 된다. 그때마다 변경된 코드가 정상적으로 동작하는지, 그리고 예상하지 못한 잘못된 작동을 유발하는지 확인하기 위해 테스트를 돌려볼 수 있으면 참 좋을 것이다.
이때 테스트 수행 속도가 느린 경우 엄청난 시간 낭비로 이어진다. 테스트의 빠른 수행 속도는 구성원 전체의 효율을 높혀 준다.

또한, 단위 테스트는 CI를 위한ㄴ 테스트에도 적합하다.
여러 사람이 참여하는 프로그램을 만들다보면 지속적인 통합 Continuous Integration은 거의 필수적이다. 여러 사람이 코드를 수행해도 안전하게 통합되게 하기 위해선 CI 과정에 Test를 넣는 것이 안전하다. CI는 여러 개발자에 의해 하루에도 몇 번이든 호출될 가능성이 높고, 이때도 빈번하게 테스트가 호출될 것인데 빠르게 결과를 확인하는데 빠른 테스트가 도움이 된다.
또한 CI과정에서 사용 시간을 기반으로 요금을 부과하는 서비스를 사용중이라면 빠른 테스트 시간은 비용 관리에도 중요할 것이다.

여담으로 브랜치 병합 과정 전에도 보통 Pull Request를 보낼 때, Gradle Build를 통한 자동화 테스트가 작동하도록 설정하는 경우가 많다.
나도 프로젝트를 하면 늘 그런 설정을 해두는 편인데, 힘들게 테스트 코드를 잘 작성해 두어도 깜빡하고 돌려보지 못하고 병합을 시도하는 경우는 항상 있다.
그렇기 때문에 사람을 믿지 말고, 코드 리뷰 전에 자동으로 테스트가 수행되도록 하고, 성공할 때만 병합될 수 있도록 하면 모두의 시간을 아낄 수 있다.

2. 자연스럽게 예제 코드가 되며 문서 역할을 하게 된다.

단위 테스트 코드는 적은 메서드나 클래스가 참여하기 때문에, 해당 클래스를 직접 만든 사람이 작성하는 경우 일종의 예제 코드가 된다!

보통 메서드 이름이나 부연 설명을 통해 동작을 설명하는데, 그 자체로 설명서가 될 수 있다. 그리고 보통 Happy Case 외에도, 다양한 예외 상황까지 코드로 검증하게 되는데 이런 테스트 작성은 곧 문서화가 된다.
구체적인 예시를 보자. 아래는 마켓 컬리 블로그에서 발견한 BDD 스타일 테스트 코드이다.

image


image


고도로 발달된 테스트 코드는 도큐먼트와 구분할 수 없다...
이런 식으로 테스트를 잘 작성해 놓으면, 해당 클래스는 어떻게 사용하는건지, 정상 동작은 어떻게 되고 어떨 때 예외가 발생하며 어떤 점을 주의해야 하는지 쉽게 알아볼 수 있다.

  1. 작성하기 쉽다.
    테스트 가능성과는 별개의 이야기로, 작은 범위만을 테스트 하기 때문에 테스트를 작성하기가 더욱 쉽다.
  2. 실패 시 원인을 파악하기 쉽다.
    검증하는 범위가 작은 만큼 하나 혹은 적은 갯수의 클래스들이 참여하기 때문에, 문제가 생기는 경우 어디서 문제가 발생했는지 찾기 쉽다. (다른 테스트 대비)

1.3 협동 테스트와 단독 테스트? 고전파와 런던파?

앞서 단위 테스트의 범위에 대해 언급했었는데, 조금 더 자세히 살펴보자.
"Unit"을 어떻게 정의하는 것이 좋을까. 유닛 테스트의 격리 수준에 대해 알아보자.
앞서 링크를 달았던 마틴 파울러의 아티클에 따르면 대표적인 격리 수준은 2가지로 나뉜다고 한다.

image

  1. Sociable Tests (협동 테스트)
  2. Solitary Tests (단독 테스트)

첫 번째 "Sociable Tests"는 협동 테스트로 번역할 수 있다. 유닛을 정의할 때 밀접한 유닛들 까지도 단일 유닛으로 정의하는 것이다. 물론, 그렇다고 해서 하나의 기능을 검사한다는 사실은 달라지지 않는다. 여러 객체 혹은 그들의 상호작용을 테스트 하고 싶다는 이야기는 아니다.
그리고 "Solitary Tests"는 테스트 대상 유닛만 테스트 한다. 테스트 하고 싶은 대상을 고립시키고, 그것만을 유닛으로 삼겠다는 것이다. 보통 다른 계층이나 객체들을 대역으로 대체하고 싶어 한다.
앞서 마틴 파울러는 정확하게 테스트할 객체 뿐만 아니라 밀접한 객체까지 유닛으로 간주한다고 했는데, 협동 테스트를 사용했다는 의미이다.
협동 테스트의 경우 대역을 만들고 구성하는 등의 여러 처리를 할 필요가 없으니 테스트 작성이 쉬워질 수 있지만, 범위가 커지고 참여 객체가 늘어나는 만큼, 다른 객체의 변화에 영향을 받을 수 밖에 없고 원인 파악이 조금 어려워질 수도 있다. 간단하게 앞서 언급한 장점들에서 얻는 이득이 살짝 낮아진다고 생각하면 된다. (이래서 장점 먼저 소개했다.)
다만 마틴 파울러는 경험상 복잡도가 그리 높지 않았고, 여러 객체가 참여하더라도 원인을 파악하는데에 문제가 없어 협동 테스트를 선호했다고 한다.
그렇다고 해서 엄격하게 "나는 협동 테스트만 할거야"라고 한건 아니고, 다른 객체와의 상호작용에 시간이 오래 걸리거나 비결정적인 경우엔 대역을 사용했다고 한다.
예를 들어 한가지 기능을 테스트 하기 위해 외부 서버 혹은 프로세스와의 연결이 필요할 수도 있는데, 이때 시간이 오래 걸리고, 외부 상황에 따라 코드는 아무런 변경이 없는데도 테스트가 실패할 수 있으므로 비결저적이다.
마틴 파울러는 이때 테스트 대역을 활용한다고 한다.

단독 테스트는 작성하기 위해서 다양한 테스트 대역들과 라이브러리 등을 이해하고 활용해야 할 수 있기 때문에 작성 자체에 조금의 허들이 있을 수 있지만, 좀 더 좁은 범위만을 고립시켜 테스트한다는 장점이 있다.

또한 대역을 Subbing 하는 과정에서 (어떤 입력이 들어오면 이렇게 대응해라. 라고 구현) 다른 객체나 다른 계층의 입력과 반환값을 정의하게 되는데, 이게 과연 자유로운 구현일까? 생각해보면 조금 아쉬운 것 같다. 좋은 구현은 낮은 의존성과 높은 응집도로 내 동작에 더 집중하고, 도메인 먼저 만들고 나중에 Persistance 부분을 구현하는 것이랬는데 아무래도 테스트를 위해서 도메인 혹은 Application 계층을 구현하면서 구체적인 Persistance의 동작이 어떻게 될지 구상해야 한다는 단점이 있을 수 있다.
한 계층이나 객체의 구현 과정에서 다른 객체의 구현을 어느 정도 확정 지어야 한다. 나중에 고치면 되잖아! 싶은데, 직접 작성해본 사람은 알겠지만 라이브러리 mokito 등을 활용한 Mocking Stubbing 코드를 수정하는 일은 매우 번거롭다. 입력이나 응답이 달라질 때마다 매번 고쳐줘야 하기 때문이다. 작은 변경에도 테스트가 쉽게 깨진다.

이러한 협동 테스트와 단독 테스트의 선호 차이는 단위 테스트를 바라보는 관점이 조금 다른 두 분파 고전파(classical school)런던파(London school)를 만들어 냈다.
런던파는 Solitary Tests를 선호하는 분파로, 테스트 할 작은 대상만을 Unit으로 보고 그 외에는 전부 대역을 세워야 한다고 생각한다. 그리고 고전파는 Sociable Tests를 선호하고, 앞서 소개한 마틴 파울러처럼 단일 클래스 또는 밀접한 클래스 세트들을 하나의 Unit으로 본다.

나는 보통 고전파의 테스트 방식을 선호한다. 런던파의 방식은 그리 많이 경험해 보진 않았지만, 최대한 대역을 사용하면서 테스트 동작 속도가 빨랐지만, 그러나 테스트 코드에 다양한 Stubbing 코드가 끼어들게 되면서 테스트 코드 자체에 대한 가독성이 나빠졌고, 쉽게 깨지는 테스트는 수정할 때마다 번거로움을 줬다.
고전파의 테스트 방식이 오히려 더 잘 맞았던것 같다. 결국 궁금한건 하나의 기능이 다른 곳의 변경 이후에도 잘 동작하는지였는데, 런던파의 방식은 요청과 응답을 직접 Stubbing해주기 때문에 변경이 제대로 반영되지 않았다. 그래서 고전파의 방식이 조금 더 믿음이 갔다. (Stubbing은 Mock 객체에 어떤 요청이 들어오면 어떻게 응답하라고 정해주는 행위이다.)
그리고 코드 작성도 조금 더 쉬웠기 때문에 결국 유지보수와 테스트 코드 신뢰의 측면에서 고전파의 방식이 조금 더 마음에 들었다.

2. 통합 테스트 (Integration Test)

통합테스트는 단위 테스트와 달리 소프트웨어를 구성하는 모듈 간의 상호작용을 검증한다.
앞서 단위 테스트가 상태와 행위를 검증하는 데에 집중했다면, 통합 테스트는 상호작용을 검증할 수 있다.

개발자가 제어할 수 없는 외부 라이브러리와의 상호작용 까지 검증할 때 사용할 수 있다. 예를 들어 DB 접근하거나, 외부 서버를 사용하는 등의 코드를 검증하고 싶을 때,
연동은 잘 됐는지, 또 예상하지 못한 다른 모듈의 변경 이후에도 프로그램이 의도대로 잘 돌아가는지는 Unit Test만으로는 테스트하기 어렵다. 더 큰 단위에서 프로그램이 제대로 동작하는지 확인하기 위해 통합 테스트를 사용할 수 있다.

따라서 단순 유닛 테스트만 테스트 과정에 포함할 것이 아니라, CI 과정에 유닛 테스트 이후 통합 테스트까지 동작하도록 만든다면 참 좋을 것이다.

단점으로는 통합테스트 자체가 여러 요소를 검증하는 만큼,

  1. 범위가 더 크고
  2. 깨지기 쉬워 신뢰하기가 어렵다.
  3. 단위 테스트에 비해 실행 속도가 느리다.

예를 들어 사용중인 라이브러리가 업데이트 되면서 동작이 바뀌거나, 필요한 설정이 추가된다면 테스트가 깨질 수도 있을 것이다. 꼭 외부의 변경이 아니더라도 작은 요소 하나의 변경에도 태스트가 실패할 수도 있기 때문에 코드 유지보수에 더 공수가 들어간다.
그리고 범위가 크다 보니 정확히 어디서 에러가 발생했는지 파악하기 어려울 수 있다.

2.1 외부 의존성 테스트

통합 테스트시 외부 의존성 테스트는 무조건 실제 외부 의존성을 호출해야 할까?

복잡한 결제 시스템을 테스트 하기 위해 매번 결제 서버에 요청을 보내게 된다면 테스트 시간이 정말 길어질 것이다.

만약 결제 서버에서 테스트용 API를 제공해준다면 테스트가 쉽겠지만, 모든 API에 대한 테스트가 제공되기는 어려울 것이다. 이런 경우 어떻게 테스트 해야 하는가?

그리고 만약 외부 서버에 장애가 발생한 상황이라면, 우리 코드엔 아무런 변경이 없음에도 불구하고 테스트가 실패하는 상황이 발생할 수도 있다. 아무런 변경 없이도 테스트 성공 실패가 바뀌는 테스트는 좋은 테스트인가

꼭 외부 서버가 아니더라도, 우리 서버에서 돌아가는 다른 프로세스와 통신하는 프로그램과의 상호작용을 테스트 할 때도, 실제 Production 환경에 직접 요청을 보내 테스트 하는 것은 테스트를 호출할 때마다 추가적인 부하로 작용할 수 있다.

물론 통합 테스트는 운영 환경과 비슷하면 비슷할 수록 좋겠지만, 이렇게 외부 의존성을 직접 호출하는 것은 여러가지 문제를 일으킬 수 있다. 이런 경우 아래와 같은 대안을 고려할 수 있다.

  1. 테스트 환경으로 대체하기
  2. 테스트 대역 사용하기



통합 테스트를 위한 환경을 구축하기 쉬운 경우 테스트 환경으로 대체할 수 있다. 클라우드 환경을 로컬에서 구현할 수 있는 LocalStack이나, 도커를 활용해 통합 테스트를 돕는 TestContainer 등을 활용하는 등의 다양한 방법을 통해 테스트 환경을 구축할 수 있다.

테스트 환경을 구축하기 어려운 경우엔 테스트 대역 - Test Duble을 활용할 수 있다.

실제 객체 대신에 사용하는 "대역"으로 자신만의 동작을 가지거나 (Fake), 어떤 입력에 대해선 어떤 응답을 줘라 (Subbing) 지시하는 방식으로 구현한다.
(다양한 Test Doubles에 대한 이야기는 이 글 참고 Test Doubles - 윤개발)

그런데, 테스트 대역을 개발자가 직접 제어한다면 이 대역의 동작이 항상 외부 시스템과 같을까? 외부 시스템의 동작이 바뀌었는데, 개발자는 모르는 상태라면- 테스트는 분명 통과하는데 실제 환경에서는 문제가 발생할 수도 있지 않는가?
따라서 보통은 외부 통신 인터페이스의 변화가 매우 적을 것으로 예상되는 경우에만 사용하는 것이 좋다.

이렇게만 보면 대역을 사용하는 방식은 조금은 위험해 보일 수도 있지만, 테스트 속도 측면에서 큰 장점이 있다. 외부 서버나 다른 프로세스를 호출하지 않고 우리 App 안의 객체들을 활용하다 보니 당연히 Test 환경을 구축하는 것 보다 압도적으로 빠를 수 밖에 없다.
내가 만든 프로젝트에서도 Redis와 통신하거나, 외부 의존성을 호출하는 부분에 Fake 객체를 구현해 테스트를 작성해 사용하고 있다.
물론 정말로 실패해야 할 때 실패하지 않으니 아쉬운 점은 있다.

2.2 Slice Test란 무엇인가

계층간 상호작용을 검증하지 않고 Test Doubles를 활용해 계층을 고립시켜 테스트 하는 방법도 있다.
검증하고 싶은 계층 외의 다른 계층의 의존성을 잘라내어 (Slice) 테스트를 하는 방식을 Slice Test라고 한다.

예를 들어 Presentation Layer의 행위만 검증하고 싶은 경우 Test Double들을 활용해 Application Layer를 대체하는 방식이다.
이러한 Slice Test 또한, 대역을 사용하므로써 다른 계층이 변경 되었을 때 테스트는 통과하므로 신뢰도가 떨어질 수도 있다.

Spring Boot에서는 Slice Test를 위한 다양한 어노테이션을 제공해준다. 아래 글에서 그 종류와 사용법을 확인해보자. 나도 Presentation Layer를 이 글에서 소개한 방식으로 테스트 했다.

3. 크기로 분류하는 방식의 제안

책 "구글 엔지니어는 이렇게 일한다"에 따르면 단위 테스트와 통합 테스트라는 용어는 모호하므로, 소형-중형-대형 테스트로 분류하는 것을 제안한다.

image

단위 테스트는 어느 것을 Unit으로 볼지 모호하고, 통합 테스트와 E2E의 구분도 모호하다.
그래서 위 피라미드의 비율은 유지하되, 아래서 부터 소형-중형-대형 테스트로 채워 나간다.
각 테스트의 정의를 알아보자.

3.1 소형 테스트 - small test

[조건]

  1. 단일 서버
  2. 단일 프로세스
  3. 단일 스레드
  4. 디스크 I/O를 사용해선 안된다.
  5. Blocking Call을 허용하지 않는다.

"단위" 테스트 보다 무엇이 되고 무엇이 안 되는지 더 명확하다. 기본적으로 고전파와 같이 엄격하게 고립시키지 않으면서도, 테스트를 느리게 만드는 디스크 I/O나 Blocking Call을 대역을 통해 허용하지 않는 것이다.
위 조건에 따르면 소형 테스트는 빠르고 결정적일 것이다. 때문에 그리고 "단위 테스트"라고 불러도 문제가 없을 것이다. (단일 스레드, 프로세스이기 때문에 TestContainer나 DB 등의 사용도 안된다.)

소형 테스트를 80% 넘게 만들어서 커버리지를 높히는 것이 이상적이다. 작기 때문에 차근차근 늘려가기도 좋다.

3.2 중형 테스트 - medium test

[조건]

  1. 단일 서버
  2. 멀티 프로세스
  3. 멀티 스레드 -> 테스트용 DB 사용 가능

소형 테스트보다 덜 엄격해졌다. 여전히 단일 서버이지만, 멀티 프로세스와 멀티 프로세스를 사용할 수 있다. 이는 TestContiainer나 테스트용 DB를 사용해도 된다는 의미이다.
따라서 이제 단위 테스트라고 부를 수 없다. 테스트의 결과가 항상 같다고 할 수 없고, 않을 수도 있고, 더 느리다

3.3 대형 테스트 - large test

  1. 멀티 서버
  2. End to End Test

일종의 API Test가 된다. 참여하는 컴포넌트가 많고, 테스트 결과가 불안정하기 때문에 가장 적은 것이 이상적이다. (피라미드 형태)

4. 인수 테스트와 E2E 테스트

4.1 E2E 테스트

End to End Test는 "끝에서 끝까지" 테스트 한다는 의미로, 어떤 기능이 동작하기 위해 참여하는 모든 컴포넌트들이 서로 올바르게 협업해 원하는 결과를 반환하는지 확인하는 테스트이다.

어떤 Endpoint와 HTTP Mthod를 가진 요청이 올바른 컨트롤러 메서드에 위임되고, 올바른 Service와 도메인, Repository에 닿아 원하던 결과가 나오는지 검증한다.
이들의 올바른 협업을 검증했다면 E2E 테스트이다.
즉, Test Duble이 참여한다면 E2E 테스트라고 말하기 어려울 것이다.

4.2 인수 테스트 (Acceptance Test)

인수테스트는 어떤 시스템이 실제 운영 환경에서 사용될 준비가 되었는지 최종적으로 확인하는 단계로, 체크리스트를 통해 비즈니스 관점에서의 사용 시나리오별로 동작을 검증한다.
소프트웨어를 인수하기 위해, 프로그램이 명세대로 작동하는지 테스트 한다는 의미이기 때문에, 클라이언트의 기대대로 작동하는지 확인하는 것이다. 보통은 개발자가 작성하기도 하지만 다른 의사소통 집단에서 작성한다고 한다. (비즈니스 이해도가 높은 사람이 작성해야..)

예를 들어 "유저가 10번 로그인 할때 마다 쿠폰이 발행된다.", "유저는 이메일을 통해 회워가입할 수 있다" 등의 기능이 제대로 동작하는지 확인하는 작업이 인수 테스트가 될 것이다.
결국 누가, 어떤 목적으로, 무엇을 하는가를 테스트 하는 것이다.
보통 내부적인 구조나, 구현 방법을 고려한다기 보다는 사용자의 관점에 집중하는 경우가 많다. "어떻게"에는 관심이 없고, A를 했을 때 B 일이 일어나면 되는 것이다.
그래서 자동화 인수 테스트는 거진 E2E 테스트와 동일한 형태로 작성된다
이러한 인수 테스트는 실제로 체크리스트를 작성하며 사이트를 방문해 테스트할 수 있고, MockMvc, RestAssured 등의 라이브러리를 통해 자동화할 수도 있다. 그리고 보통 예외적인 상황 보다는 Happy Case를 작성한다. (제대로 작동할 때의 동작)

4.3 차이가 뭘까?

결국 관점의 차이인 것 같다. 어떤 기능이 제대로 동작하는지 확인하는 것은 비슷하지만, 관점이 조금 다르다.

  • 인수 테스트 : 비즈니스적으로 클라이언트의 요구사항을 제대로 만족하는지? (인수하기 위한 테스트!)
  • E2E 테스트 : 기술적인 관점에서 어떤 기능이 제대로 작동하는지? 참여하는 모든 컴포넌트들이 제대로 상호작용을 해내서 원하는 결과를 반환하는지?

Reference