Search

더 나은 테스트 방식에 대한 생각

들어가며

스타트업인데 테스트에 왜 리소스를 투입하면서까지 작성하는걸까요?
한 간담회에서 스피커로서 그리고 어딘가에 투고한 저의 회고글에서 테스트에 대한 제 생각을 얘기했던 적이 있습니다.
거기서 저는 TDD든 NOT TDD든 상관없다는 얘기를 했었고 테스트만 작성한다면 괜찮다는 생각은 변함이 없습니다.
여태까지 약 2500개의 테스트 케이스를 작성하는동안 단 하나도 TDD를 지켜서 작성하지 않았지만 작성 해놓은 테스트의 덕을 본 경험은 정말 많았습니다.
아래부터는 테스트에 대해 정리하며 제 생각을 작성해보려합니다.

테스트란?

테스트란 어플리케이션이 사용자가 요구하는 기능의 동작과 성능, 사용성, 안정성 등을 만족하는지 확인하며 소프트웨어의 결함을 찾아내는 활동입니다.
출처 : GeeksforGeeks
시스템 개발 과정을 시각화한 소프트웨어 개발 생명 주기 모델 중 하나인 V 모델을 살펴보면 개발 과정과 그에 맞는 테스트 종류를 한 눈에 확인할 수 있습니다.
개발 과정에 따른 테스트 종류 말고도 범위에 따라서 단위 테스트, 통합 테스트, E2E 테스트로도 구분할 수 있습니다.
테스트란 결국 어플리케이션을 검증하고 결함을 찾아내는 것이지만 개발 과정에 따라, 범위에 따라 그 종류를 나눌 수 있고 각 종류마다 고유한 장단점이 존재하여 상황에 맞는 적절한 테스트를 선택해야합니다.

단위 테스트

단위 테스트란 일반적으로 사용되는 테스트 중 하나로 개별적인 코드 단위(함수, 메서드)가 의도한 대로 정확한 입력값과 출력값을 반환하는지 확인하는 과정입니다.
//Calculator.class public class Calculator { public int add(int a, int b) { return a + b; } } //CalculatorTest.class public class CalculatorTest { @Test public void test() { Calculator calculator = new Calculator(); int result = calculator.add(1, 2); Assertions.assertThat(result).isEqualTo(3); } }
Java
복사
위 예시 코드의 Calculator 클래스는 add 함수를 통해 숫자 a, b를 받아서 더해주는 기능을 합니다.
이 add 함수에 대한 단위 테스트는 아래의 CalculatorTest 처럼 임의의 숫자를 전달했을 때 정상적인 값이 반환되는지 확인합니다.
만약 값이 같다면 테스트가 성공하고 값이 다르다면 테스트가 실패합니다.

통합 테스트

통합 테스트는 서로 다른 모듈들 간의 상호작용을 테스트하는 과정입니다.
예를 들어 API 서버의 DB 호출 함수가 DB의 데이터를 잘 호출하는지 테스트하는 과정을 생각하시면 됩니다.
보통 통합 테스트는 서로 다른 모듈들 간의 상호작용을 테스트하기 위해 각 모듈의 단위 테스트를 모두 완료한 뒤 둘 이상의 모듈을 거쳐서 동작하는 API 테스트 시나리오를 기반으로 통합 테스트를 수행합니다.
//UserService.class public class UserService { private final UserRepository userRepository; public List<UserDto> findAll() { List<User> users = userRepository.findAll(); List<UserDto> data = convertToDto(users); return data; } } //UserIntegrationTest.class public class UserIntegrationTest { private final UserService userService; @Test public void findAll() { List<UserDto> users = userService.findAll(); Assertions.assertThat() //내용생략 } }
Java
복사
위 예시코드의 UserService 클래스의 findAll은 DB에서 전체 유저를 조회해서 Dto로 변환 후 반환해주는 기능을 하고 있습니다.
통합 테스트는 이런 식으로 DB 데이터 조회와 변환 작업을 한꺼번에 테스트하는 방식으로 테스트를 작성합니다.

E2E 테스트

E2E(End To End) 테스트는 어플리케이션의 처음부터 끝까지 테스트하는 과정입니다.
유닛 테스트나 통합 테스트를 통해 검증이 되었어도 실제 사용자의 사용 시나리오를 검증 할 수 없기 때문에 E2E 테스트를 수행하여 어플리케이션이 제대로 동작함을 보장합니다.
예를 들어 유저가 자신의 정보를 조회하려는 경우 유저는 가입과 로그인을 하는 과정이 선행되어야합니다.
//FindUserProfileTest.class public class FindUserProfileTest { private String token; private final UserRepository userRepository; @BeforeEach public void setUp() { User user = UserUtils.createUser(email, password); userService.register(email, password); LoginResult result = userService.login(email, password); token = result.getToken(); } @Test public void findUserProfile() { EntityExchangeResult<ResponseDTO<UserProfileDto>> response = webTestClient .get() .uri("/v1/users/{userId}/profile", user.getId()) .header("Authorized", token) .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isEqualTo(HttpStatus.OK.value()) .expectBody(new ParameterizedTypeReference<ResponseDTO<UserProfileDto>>() {}) .returnResult(); UserProfileDto body = Objects.requireNonNull(response.getResponseBody()).getData(); Assertions.assertThat() //내용 생략 } }
Java
복사
위 예시코드는 @BeforeEach 어노테이션이 붙은 setUp() 메서드를 통해서 먼저 유저를 생성하여 가입시키고 로그인하는 과정을 처리하고 있습니다.
이후 @Test 어노테이션이 붙은 findUserProfile() 메서드에서 유저 정보를 조회하는 API를 해당 유저의 토큰으로 조회한 후 정상적으로 조회된건지 검증합니다.
테스트를 통해 유저 시나리오를 만들고 실제 사용자처럼 API를 호출함으로써 인증/인가 필터부터 비즈니스 로직 그리고 DB와의 커넥션까지 모두 한꺼번에 테스트할 수 있습니다.

테스트 방법론

TDD

TDD란 Test Driven Development의 약자로 테스트 주도 개발을 의미합니다.
그 의미에 맞게 테스트가 주가 되어서 테스트 코드를 먼저 작성한 후 실제 코드를 작성하는 방법론입니다.
위 사진은 TDD의 테스트 코드 작성 방식을 보여줍니다.
Write a failing test
실패하는 테스트 코드를 작성합니다.
실패하는 것은 아직 실제 코드가 작성되지 않았거나 미완성이기에 실패하는 것이지 테스트 코드에 문제가 있는건 아닙니다.
Make the test pass
테스트 코드를 성공시키기 위해 실제 코드를 작성 혹은 변경합니다.
테스트 코드를 성공시키기 위한 최소한의 코드만 변경합니다.
Refactor
실제 코드를 깔끔하게 정리하기 위한 리팩토링을 진행합니다.

BDD

BDD란 Behavior Driven Development의 약자로 행위 주도 개발을 의미합니다.
BDD는 TDD를 근간으로 파생된 개발 방법론으로 비즈니스 요구사항에 집중하며 사용자 행동 시나리오를 기반으로 테스트 케이스를 작성합니다.
TDD를 근간으로 한 만큼 테스트를 작성하는 과정은 동일하지만 비개발직군이 보더라도 이해하기 쉬운 Ubiquitous한 테스트 케이스와 Given-When-Then이라는 기본 패턴을 권장합니다.
Feature
테스트에 대상의 기능/책임을 명시합니다.
Scenario
테스트 목적에 대한 상황을 설명합니다.
Given
시나리오 진행에 필요한 값을 설정합니다.
When
시나리오를 진행하는데 필요한 조건을 명시합니다.
Then
시나리오를 완료 했을 때 보장해야 하는 결과를 명시합니다.

그럼 TDD, BDD 뭘 써야해?

TDD든 BDD든 혹은 그 다른 것이든 그 어떤 테스트 방법을 사용해서 테스트를 작성해도 상관없다고 생각합니다.
또한 그 둘을 함께 사용해도 상관없습니다.
출처 : kakaoif 2020 - kotest가 있다면 TDD 묻고 BDD로 가
kakaoif 컨퍼런스 발표를 보면 TDD와 BDD는 서로 상호보완적인 관계라고 합니다.
BDD는 TDD에서 확인하기 어려운 유저 시나리오의 흐름을 알 수 있고, TDD는 각 모듈의 기능을 검증할 수 있기 때문입니다.
즉, BDD의 테스트 케이스로 시나리오 검증을 하고 해당 시나리오에서 사용되는 각 모듈들은 TDD의 테스트 케이스로 검증하면 테스트 커버리지를 높일 수 있습니다.
TDD는 RED-GREEN-BLUE의 과정을 반복하며 실제 코드에 대한 검증을 진행하기 때문에 Agile한 개발 프로세스에서 단위 테스트에 적합한 방법이라고 생각합니다.
그리고 BDD는 테스트의 대상이 사용자 행동 시나리오이기 때문에 E2E 테스트에 적합한 방법이라고 생각합니다.
개인적으론 테스트 코드만을 보고도 서비스나 기능을 어느정도 파악할 수 있어야 한다는 생각을 갖고 있는데 BDD의 방식이 가장 적합한 것 같아서 좀 더 선호합니다.

테스트 작성 패턴

테스트 작성 패턴은 테스트를 표현하는 스타일입니다.
그 중 앞서 말했던 BDD에서 권장하는 패턴인 Given-When-Then이 있고 마틴 파울러는 이러한 패턴이 Four-Phase 패턴의 재구성이라고 말합니다.
또한 Given-When-Then은 Gherkin 문법이라고도 불리며 BDD based Test Tool인 Cucumber에서 제안한 테스트 문법입니다.
Four-Phase 패턴과 Given-When-Then 패턴은 아래처럼 비교 해볼 수 있습니다.

Four-Phase

setup
테스트 대상 시스템(일반적으로 클래스, 객체 또는 메서드)이 설정됩니다.
exercise
테스트 대상인 시스템이 실행됩니다.
verify
테스트 결과가 개발자의 기대치와 일치하는지 검증합니다.
teardown
테스트 대상 시스템을 설정 전 상태로 재설정합니다.

Given-When-Then

given
테스트를 하기 위한 기본적인 설정입니다.
when
테스트를 하기 위한 조건을 설정합니다.
then
테스트가 예상한대로 동작하는지 검증합니다.
아래는 Given-When-Then 패턴을 사용한 예제 코드입니다.
@DisplayName("유저 나이 테스트") public class UserTest { private static final int ADULT_AGE = 20; @Test @DisplayName("해당 유저가 성인인지 확인하는 테스트") void checkAdult() { //Given User user = new User(); //When String age = user.getAge(); //Then Assertions.assertThat(age).isGreaterThanOrEqualTo(ADULT_AGE); } @Test @DisplayName("해당 유저가 성인이 아닌지 확인하는 테스트") void checkChild() { //Given User user = new User(); //When String age = user.getAge(); //Then Assertions.assertThat(age).isLessThan(ADULT_AGE); } }
Java
복사
위 예제의 테스트들을 실행하게 되면 아래처럼 표시됩니다.
여기까지만 보고도 무언가 불편함을 느끼시는 분이 계실 수도 있습니다.
Given-When-Then 패턴은 하나의 테스트 메서드 안에 테스트를 하기 위한 설정, 조건, 검증 등의 코드를 모두 갖고 있습니다.
예제처럼 짧은 코드의 테스트는 각 섹션의 코드를 한 눈에 쉽게 확인할 수 있지만 코드가 길어질수록 각 섹션을 구분하여 확인하기 힘들 정도로 가독성이 떨어집니다.
또한 클래스와 메서드의 스코프만으로는 테스트 하려는 시나리오를 표현하기에 부족한 느낌이 들어 테스트 대상 행동에 대해 더 구체적이고 분리된 테스트를 하고 싶은 생각이 듭니다.
오랜기간동안 Given-When-Then 패턴을 사용하며 이러한 불편함들을 느꼈던 저는 DCI 패턴을 알게된 후 기존 테스트 코드들까지 리팩토링 할 정도로 DCI 패턴에 매료되었습니다.

DCI 패턴

DCI는 Describe-Context-It 을 의미하며 정확하게 하나의 테스트 메서드에 대한 행위를 검증하는데 집중하여 코드의 행동을 설명하는 테스트 코드를 작성하도록 도와줍니다.
그래서 Given-When-Then 패턴과는 다르게 실제 테스트 대상을 구체적으로 설명하는 테스트 케이스를 작성하는데 집중할 수 있습니다.
Describe
설명해야 할 테스트의 대상을 명시합니다.
Context
테스트를 위한 특정한 상황이나 조건을 설명합니다.
It
특정한 상황이나 조건에서 기대하는 결과를 검증하며 테스트 대상의 행위를 설명합니다.
아래는 DCI 패턴을 사용한 예제 코드입니다.
@DisplayName("주문 테스트") public class OrderTest { ... @Nested @DisplayName("주류 주문은") class Describe_of_Save_Order_For_Alcohol { ... @Nested @DisplayName("미성년자인 경우") class Context_with_Not_Adult_User { ... @Test @DisplayName("주문할 수 없다.") void it_fail_Order() { ... } } } }
Java
복사
DCI 패턴은 JUnit5의 @Nested 어노테이션으로 Inner Class를 만들어 계층형 구조의 테스트 케이스를 작성합니다.
확실히 이전보단 테스트 하려는 행위에 대해 구체적으로 설명하는 테스트가 만들어졌습니다.
개인적으로 가장 매력적인 부분은 @DisplayName으로 표시한 이름을 쭉 읽을 때 하나의 문장으로 설명할 수 있는게 아닐까 생각합니다.
또한 유저를 생성하고 로그인하는 것처럼 공통적으로 사용하는 코드는 상위 클래스에 두어 중복되는 코드를 줄일 수 있다는 점도 편리합니다.
그럼 이제 테스트는 어떻게 작성할지 알았는데 테스트에 사용하는 상황이나 조건은 어떻게 설정할까요?

테스트용 더미 데이터

테스트에 사용할 상황이나 조건을 만드려면 더미 데이터가 필요하고 더미 데이터를 만드는 방식은 여러 가지가 존재합니다.
1.
SQL 스크립트 실행
2.
DB 프로시져 실행
3.
ApplicationRunner를 통한 생성
4.
Entity 객체를 이용한 생성
등등..
위 방식들을 크게 2가지로 나눠본다면 더미 데이터를 미리 생성하는 방식과 필요할 때 생성하는 방식으로 나눌 수 있을 것 같습니다.

미리 생성하는 방식

INSERT INTO `user` (`id`, `email`, `password`, `age`) VALUES (1, 'tester1@test.com', 'password', 20), (2, 'tester2@test.com', 'password', 20), ...
SQL
복사
위 스크립트처럼 SQL 스크립트를 통해 더미 데이터를 생성하는 방식은 미리 생성하는 방식에 해당합니다.
저는 오랫동안 이런 방식으로 테스트를 작성해왔고 몇몇 문제점들을 발견했습니다.
1.
테스트가 더미 데이터에 종속되는 현상
만약 A 테스트에서는 유저가 성인인지 테스트하고, B 테스트에서는 20살인 유저를 찾는 테스트를 하게 되었을 때 더미 데이터의 age를 수정하게 되면 A, B 테스트 둘 다 영향을 받게 됩니다.
이런식으로 더미 데이터의 변경으로 인해 의도치 않은 다른 테스트까지 영향이 가는게 좋아보이지 않았고 앞으로 여러 도메인이 복잡하게 얽히는 기능들이 더 추가된다면 문제가 심각해질거라 생각했습니다.
2.
다른 사람은 알아보기 어려운 구조
SQL 스크립트엔 모든 테스트에 사용하는 더미 데이터를 만들기 위해 많은 테이블에 많은 데이터들이 있을 것입니다.
그 중에서도 몇몇 테스트 케이스를 만족하기 위해 특별한 조건들을 만들어내는 유의미한 데이터들도 있을 것입니다.
다른 사람이 SQL 스크립트를 하나하나 읽어보면서 그걸 파악할 수 있을까요? 저는 그럴 필요성 조차 없다고 생각했습니다.

필요할 때 생성하는 방식

public class UserUtils { public static User createUser(String email, String password) { return User.builder() .email(email) .password(password) .age(20) ... .build() } }
Java
복사
위 예제 코드는 static 메서드를 통해 필요한 데이터를 생성하는 Util Class를 보여주고 이러한 방식이 필요할 때 생성하는 방식에 해당합니다.
JPA 같은 ORM을 사용한다면 Entity 단위로 필요할 때마다 필요한만큼 더미 데이터를 생성할 수 있습니다.
하지만 이런 ORM 기술을 사용하려면 해당 기술에 대한 이해가 필요하기 때문에 필요성을 느끼지 못한다면 NativeSQL을 사용한다든가 다른 방식을 사용할 수도 있을 것 같습니다.
스프링 테스트에서는 테스트 단위로 트랜잭션을 관리하여 테스트가 종료되면 트랜잭션을 rollback시켜 tear-down에 대한 코드가 필요하지 않습니다.
혹시 이런 Automated tear-down이 지원되지 않는 다른 언어나 프레임워크를 사용한다면 테스트 환경과 조건을 원래 상태로 되돌려놓는 코드를 작성해주어야합니다.
만약 위 예제 코드에서 나이가 19살인 유저에 대한 더미 데이터가 필요해지면 어떻게 해야 할까요?

특정 조건 만들기

public class UserUtils { public static User createUserOfAgeEqualTo20(String email, String password) { return User.builder() .email(email) .password(password) .age(20) ... .build() } public static User createUserOfAgeUnder20(String email, String password) { return User.builder() .email(email) .password(password) .age(19) ... .build() } }
Java
복사
이런식으로 Util Class에 특정 조건을 가진 static 메서드를 하나 더 추가해야할까요?
public class UserUtils { public static User createUser(String email, String password, int age, ...) { return User.builder() .email(email) .password(password) .age(age) ... .build() } }
Java
복사
이런식으로 createUser 메서드에 인자를 통해 넘겨주어야할까요?
public class User { private String email; private String password; private int age; ... public void setAge(int age) { this.age = age; } }
Java
복사
이런식으로 생성한 User Entity에서 특정 조건을 만들어주기 위한 setter 메서드가 필요할까요?
검증하려는 조건이 많아진다면 결국 같은 User Entity를 만드는 함수들이지만 여러 종류의 함수가 생겨나면서 복잡도가 오히려 증가할 수 있습니다.
또한 메서드의 인자가 5개가 넘어가면 가독성도 떨어지고 잘못된 인자를 넘길 수 있기 때문에 지양하는게 좋습니다.
다른 언어들처럼 Java도 Named Parameter를 지원해준다면 생각이 달라질 수도 있겠지만 아쉽게도 Java는 아직 지원해주지 않습니다.
(17년 Google 개발자들이 참여한 논문인 Detecting Argument Selection Defects에도 비슷한 내용이 있습니다.)
그리고 테스트만을 위한 setter 메서드를 만드는 것은 실제 코드와 테스트 코드의 주객이 전도된 것입니다.
이것은 잘못 작성한 테스트 코드를 성공시키기 위해 실제 코드를 잘못 작동하도록 수정하는 것과 같다고 생각합니다.
그러면 어떤 방식으로 특정 조건을 만들어 줄 수 있을까요?
@DisplayName("주문 테스트") public class OrderTest { ... @Nested @DisplayName("주류 주문은") class Describe_of_Save_Order_For_Alcohol { ... @Nested @DisplayName("미성년자인 경우") class Context_with_Not_Adult_User { @BeforeEach void setUp() { //Reflection 사용 예제 try { Field field = user.getClass().getDeclaredField("age"); field.setAccessible(true); field.set(user, 19); } catch (Exception e) { throw new RuntimeException(e); } } @Test @DisplayName("주문할 수 없다.") void it_fail_Order() { ... } } } }
Java
복사
위 예제 코드처럼 Java의 Reflection을 사용할 수 있습니다.
Reflection은 Runtime에 클래스 타입을 CompileTime에 지정한 것처럼 사용할 수 있게 지원해주는 API입니다.
이걸 이용하면 Java의 Class를 조작할 수 있고 private 필드 혹은 final 필드여도 그 값을 변경할 수 있습니다.
Java의 Reflection은 느리고 위험하고 복잡하다는 오해가 많습니다.
물론 Reflection은 컴파일러에 의해 최적화되지 않고 동적으로 타입을 지정하고 JVM에 바이트코드로 존재하는 Class들의 정보를 조작하는 것이기 때문에 원하지 않은 실행 결과를 만들어낼 수 있습니다.
하지만 적절한 곳에 적당히 사용한다면 강력한 기술이 될 수 있기 때문에 이런 단점들에 영향을 그나마 덜 받는 테스트 환경에서 사용하는 것은 괜찮다고 생각했습니다.
여기까지 테스트에 대해서와 어떻게 작성하는지에 대해 알아보았습니다.

그래서 테스트를 왜 작성해야해?

테스트의 필요성에 대한 주제는 갑론을박이 많은 주제입니다.
처음부터 완벽하게 코드를 작성한다면 사실 테스트는 필요하지 않을 수 있습니다.
하지만 제일 조심해야 하는 것은 스스로이기도 하고 자기가 작성한 코드가 그나마 완벽하다고 생각하기 위해선 테스트는 필수적으로 작성해야 하지 않을까 생각합니다.
물론 상황과 환경에 따라서 테스트의 필요성이 커질 수도 작아질 수도 있습니다.
그 중에서도 자체적으로 운영하는 서비스를 개발하는 곳 그리고 많은 유저를 대상으로 하는 서비스라면 그 필요성은 커질 수도 있을 것 같습니다.
적은 리소스로 빠르게 가설을 검증해야하는 스타트업이라면 빠른 변화에 안정적으로 대응하기 위해서 더더욱 테스트가 필요할 수도 있습니다.
많은 곳에서 테스트의 장점으로 아래와 같은 이야기들을 합니다.
코드 품질 향상
에러 조기 발견
문서화로서의 역할
코드 수정 시 안정적인 대응
리팩토링 시 동일한 기능 보장
시간과 비용 절감
등등..
하지만 이런 통상적인? 보편적인? 여러 곳에서 나열된 장점들보단 직접 느꼈던 경험들을 얘기해보려합니다.
우선은 자신의 코드에 자신감이 생깁니다.
단순한 말일 수 있지만 자신감을 갖고 있는 것과 아닌 것은 큰 차이가 있다고 생각합니다.
내가 작성한 코드가 잘 작동할거라는 자신감이 모여서 하나의 서비스, 프로덕트만큼 커지게 되고 자연스럽게 오너십과 애착을 가지게 되어 결국 긍정적인 방향으로 이어진다고 느끼고 있습니다.
그리고 사이드 이펙트를 빠르게 감지하고 대응할 수 있습니다.
개발을 하다보면 외부에서 들어오는 변경요청도 많지만 스스로에게서 끓어오르는 변경요청도 존재합니다.
계속해서 여러 가지를 시도해보고 끊임없이 개선해나가는걸 지향하는 저는 이러한 변경사항에 대해선 저 스스로가 가장 무서운 사람입니다.
하지만 현실은 바꾸고 싶다고 마음대로 바꾸면 수많은 에러를 마주하게 됩니다.
이런 많은 순간에 하나하나 작성해가면서 쌓아뒀던 테스트 케이스들이 저의 생명을 연장해줬다고 생각합니다.
마지막으로 빠르게 퇴근할 수 있게 됩니다.
테스트를 작성하게 되면 굳이 사람 손으로 테스트를 할 필요 없이 자동화된 테스트를 할 수 있게 되고 조기에 에러를 발견하여 더욱 완성도 있는 코드를 작성할 수 있게 됩니다.
이렇게 증가한 생산성으로 아껴지는 시간들을 모으면 점점 더 빠르게 퇴근할 수 있게 됩니다.

그렇다고 TDD, BDD를 꼭 해야하는가?

저는 둘 다 지키지 않습니다.
TDD, BDD는 개발보다 먼저 테스트를 작성하자는 방법론이지만 개인적으론 개발과 테스트의 순서는 중요하지 않다고 생각합니다.
개발자는 주어진 문제를 시간 안에 해결하는 사람이기에 테스트를 작성하기 위해 듀데이트를 지키지 못하는건 개인적인 욕심이지 않을까 싶습니다.
그래서 저는 오히려 선개발 후테스트를 지향하는데 테스트는 언제가 됐든 작성만 되어있으면 그 이후부턴 테스트를 작성함으로써 얻는 이점을 모두 누릴 수 있기 때문입니다.

그러면 어떤 테스트를 작성해야하는가?

당연히 모든 종류의 테스트를 작성하는 것이 좋습니다.
하지만 저희에겐 항상 무한한 시간이 주어지지 않으니 선택과 집중을 해야하고 그 중에선 E2E 테스트로 최대한 많은 케이스를 만드는게 아직까진 좋다고 생각합니다.
읽어주셔서 감사합니다.
이 글에서의 저의 생각은 더 좋은 테스트 방식을 찾아가기 위한 과정이기 때문에 정답이지 않습니다.