단위 테스트(Unit Test)
테스트 목적
일반적인 의미
- 어떠한 일에도 잘 작동되는지에 대한 검증이 필요
Java 기반 애플리케이션 테스트
- Postman을 통해 JSON 응답 결과 확인하는 것 또한 테스트
- breakpoint를 걸어서 라인 단위로 확인
보다 간편한 애플리케이션 테스트
- 비즈니스 로직에서 특정한 메서드나 특정 계층을 테스트하기 위한 편리한 테스트
- 작은 단위로 테스트를 할 수 있도록 하는 테스트가 바로 단위 테스트
단위 테스트
- 기능 테스트
- 가장 큰 테스트 범주
- 애플리케이션을 사용하는 사용자가 제공하는 기능을 올바르게 작동하는지 테스트
- 개발자 혹은 QA 부서 등이 테스트
- API 툴과 데이터베이스가 연관되어 얽혀 있는 테스트
- 통합 테스트
- 주로 개발자가 테스트
- 클라이언트 툴 없이 개발자가 짠 코드 테스트
- 여러 애플리케이션 계층이 연관되고 DB까지 연결되어 있는 테스트
- 슬라이스 테스트
- 애플리케이션의 특정 계층을 테스트
- API 계층, 서비스 계층, 데이타 베이스 계층 등등
- HTTP 요청과 외부 서비스와 연동되어 있는 테스트
- 단위 테스트
- 애플리케이션 핵심 로직인 클래스들을 단위 테스트 대상으로 본다
- 주로 독립적으로 작동하는 대상
- 주로 클래스의 메서드를 테스트
- 메서드 단위로 작성
단위 테스트 목적
- 매번 postman등을 사용하는 비효율 개선
- 코드 동작 확인
- 작은 단위로 미리 확인 가능하여 빠른 시간에 문제 원인 소명
- 테스트 케이스를 만들어 빠르게 문제 해결
FIRST 원칙
- Fast
- 테스트 케이스는 빠르게 돌아가야 한다
- Independent
- 각각의 테스트 케이스는 독립적
- 서로 영향을 주지 않아야 한다
- Repeatable
- 반복 실행하여 같은 결과를 얻을 수 있어야 한다
- 외부 서비스의 영향을 받지 않기 위해 연동을 끊는다
- Timely
- TDD(테스트 주도 개발)에서 기능 구현 전 테스트 케이스 먼저 작성
JUnit 없이 단위 테스트 적용
- helper 클래스와 utility 클래스 사용
- utility 클래스는 인스턴스화 될 필요가 없는 static method로 구성
- 예시
- 테스트 대상이 되는 helper 클래스를 하나 만든다
- 새로운 값과 기존 값을 받아 더해서 return 하는 메서드를 가지고 있다.
- 테스트를 하는 클래스를 만들어서 main을 작성
- 테스트하는 클래스에 테스트 메서드를 만들어서 기대했던 값을 작성하고 맞는지 확인하고 main에서 호출
- 테스트 대상이 되는 helper 클래스를 하나 만든다
- given-when-then 방식의 BDD(Behavior Driven Development) 테스트 방식에서 사용
- given - 테스트 준비과정, 전제 조건, 입력값
- when - 테스트 동작 대상, 메서드 호출
- then - 테스트 결과 검증, expected와 actual 비교하여 assertion(검증)하는 코드
JUnit 사용 테스트
JUnit 비즈니스 로직에 적용
JUnit
- Java로 만들어진 애플리케이션을 테스트하기 위한 표준 테스트 프레임워크
- 2022년 현재 JUnit5 릴리즈
JUnit 기본 작성법
- Spring Boot Initializr에서 Spring Boot 프로젝트를 생성하고 src/test 디렉터리가 생성되면 그 하위에 테스트 케이스 작성
- testImplementation 'org.springframework.boot:spring-boot-starter-test' 이 자동으로 포함
JUnit 테스트 케이스 기본 구조
- @Test 애너테이션을 붙여서 테스트하고자 하는 대상에 대한 테스트 로직 작성
- Assertion method 사용
- src/test 아래에 클래스를 생성
- @Test 애너테이션을 붙여 테스트 케이스로 만든다
- @DisplayName(””)을 작성하여 테스트 이름을 작성
- assertEquals(T a, T b) a와 b가 같은지 확인하는 메서드
- 테스트 전체를 실행해도 되고 특정 테스트 케이스를 실행 가능
- show passed 버튼을 클릭하여 passed 결과를 확인 가능
- 값이 같으면 초록 체크 표시
- 값이 다르면 노란색 x표시(기대 값과 실제 값을 표시)
Asserction method
- assertNotNull() : Null 여부 테스트
- assertThrows() : 예외 테스트 - 예외 발생하는지 테스트
- 첫 번째 파라미터에 예상되는 예외를 넣고, 두 번째 파라미터에 대상 메서드를 람다식으로 작성
- 예상되는 예외는 발생하는 예외의 상위 타입을 넣어도 작동한다
- assertDoesNotThrow()
- 대상 메서드를 람다식으로 작성
- assertTrue()
- 참인 경우인지 확인
테스트 케이스 실행 전 전처리
- @BeforeEach 애너테이션을 붙인 메서드를 생성하면 그 안에 있는 테스트들을 실행하기 앞서 그 메서드를 사전에 각 테스트마다 각각 실행해준다.
- @BeforeAll 애너테이션을 붙인 메서드를 생성하면 테스테 케이스 전체 실행할 때 초기에 한번만 실행
- @BeforeAll 애너테이션을 추가한 메서드는 static 메서드여야 한다.
테스트 케이스 실행 후 후처리
- @AfterEach, @AfterAll 애너테이션 지원
- 테스트 케이스 종료 시점에 작동
Assumption을 이용한 조건부 테스트
- assumeTrue()
- true 값을 가지면 나머지 이하 로직 실행
Hamcrest Assertion 적용
Hamcrest
- JUnit 기반 단위 테스트에서 사용할 수 있는 Assertion framework
- JUnit에서 Assertion 메서드를 다양하게 지원하지만 Assertion 메서드보다 많이 사용
- mather가 자연스러운 문장으로 이어진다.
- 테스트 실패 메시지 가독성 상승
예시 1
- assertEquals(expected, actual) → assertThat(actual, is(equalTo(expected)))
- 하나의 문장으로 인식 가능(assert that actual is equal to expected)
- 실제 값이 기댓값이 되어야 하는 것을 검증하다
- 실행 결과
- failed의 경우보다 자연스럽게 인식 가능
- Expected: is “…”
- but: was “…”
예시 2
- assertNotNull(actual, "") → assertThat(actual, is(notNullValue()))
- 보다 인식하기 쉬운 형태로 변형 가능
- 실행 결과
- failed의 경우 예시 1과 동일한 형태
예시 3
- assertThrows() → assertThat(actual.getCause(), is(equalTo(null))
- assertThrows를 사용하여 얻은 값을 Throwable 객체에 저장
- assertThat에서 Throwable 객체에서 getCause를 통해 값을 얻고 null인지 확인
슬라이스 테스트
- 각 계층에 구현한 기능이 작동하는지 특정 계층만 잘라 테스트하는 것
- 스모크 테스트 : 전체적인 기능 테스트 전 특정 수정 사항으로 영향을 받을 수 있는 범위에 제한된 테스트
API 계층 테스트
- 클라이언트의 요청을 받는 controller
- controller 클래스 테스트 구조
- 테스트 클래스에 애너테이션 추가
- @SpringBootTest - application context 생성
- @AutoConfigureMockMvc - 자동 구성 작업
- 멤버 변수로 MockMvc 객체 선언 - spring MVC 테스트 프레임 워크(@Autowired로 의존성 주입)
- 이후 @Test 애너테이션을 붙인 테스트 케이스 작성
- given - request body 데이터를 생성
- when - request를 수행
- then - 검증 작업
- 테스트 클래스에 애너테이션 추가
- 구체적인 구현
- given
- dependencies에 implementation 'com.google.code.gson:gson’ 추가
- 필드 멤버에 Gson 객체를 생성
- Dto 객체를 받아서 gson.toJson()를 사용해 JSON 형태로 변환
- when
- ResultActions 객체를 선언하고 mockMvc.perform(); 수행한 값을 할당
- mockMvc.perform 안에 post([URL]). accept(). contentType(). content() 속성 설정
- accept (MediaType.APPLICATION_JSON) 클라이언트 쪽에서 리턴 받을 응답 데이터
- contentType(MediaType.APPLICATION_JSON) 서버 쪽에서 처리 가능한 데이터 타입
- content()를 통해 request Body 설정
- then
- MvcResult 객체를 선언하고 앞에서 만든 ResultActions 객체에서 .andExpect(status().isCreated())처럼 상태 값이 created인지 검증하는 코드 작성
- .andExpect(jsonPath().value())를 사용하여 각 속성 값 검증
- jsonPath(”$.data.email”)로 작성하면 josn 형식의 response body에서 data에 email에 있는 값을 가져오고 value를 사용하여 기대하는 값을 넣어 비교
- .andReturn()을 사용하면 response로 전달되는 응답 반환 가능
- System.out.println(result.getResponse().getContentAsString());
- MvcResult 객체에서 getResponse를 통해 값을 가져오고 getContentAsString을 통해 문자열 가져와서 출력 가능
- given
- 참고
- JSON 데이터 한글이 깨질 경우
- application.yml
- 위의 코드 추가
- server: servlet: encoding: force-response: true
- 문제점
- controller와 연결된 데이터 액세스 계층의 불필요한 로직이 수행된다
- Mock 객체를 사용하여 문제 해결 가능
- @WebMvcTest를 이용한 테스트
- @SpringBootTest, @AutoConfigureMockMvc를 사용하는 것에 비해 controller에 의존하는 컴포넌트를 모두 일일이 수정해야하는 불편함이 존재
데이터 액세스 계층 테스트
- JPA에 대한 테스트를 진행
- 데이터 액세스 계층 테스트 규칙
- DB 상태를 테스트 케이스 실행 이전으로 돌려서 사용
- 데이터 엑세스 계층 테스트 구현
- 테스트 클래스에 @DataJpaTest 애너테이션을 붙인다
- @DataJpaTest는 @Transactionial 애너테이션을 포함하고 있기 때문에 테스트 케이스가 하나 종료되면 저장된 데이터는 rollback 처리가 된다.
- repository 객체를 가져와서 @Autowired로 dI 처리
- @Test를 붙인 테스트 케이스를 작성
- given
- 엔티티 객체를 생성해서 초기화
- when
- save를 사용하여 DB에 추가하고 결과 값 저장
- then
- assertNotNull을 사용해서 결과 값이 null이 아닌지 확인
- 저장된 객체와 기존 객체의 속성들을 비교하는 assertTrue, assertEquals 등 사용
- given
- 테스트 클래스에 @DataJpaTest 애너테이션을 붙인다
Mockito
Mock
- 실제 제품이 나오기 전 모형
- 테스트에서는 가짜 객체를 의미
Mock 객체 사용 목적
- mock 객체 없이 슬라이스 테스트 시 특정 계층이 아니라 전체적인 계층에서 작동한다.
- 테스트는 빠르고 단위가 작을수록 좋기 때문에 불필요한 계층을 거치는 것을 방지하기 위해서 사용한다.
Mockito
- Spring Framework에서 지원하고 있는 mocking 라이브러리가 Mockito
슬라이스 테스트 Mockito 적용
- 기존 슬라이스 테스트에서 @MockBean 애너테이션을 추가한 서비스 계층 객체를 생성
- mapper 객체를 필드로 가져온다.
- mapper를 사용해 Dto 형태로 가져온 데이터를 member 객체로 변환
- given(memberService.createMember(Mockito.any(Member.class)))
- given()은 mock 객체가 특정값을 리턴하는 동작을 지정
- 서비스 계층 객체인 memberService를 mock으로 선언하고 createMember 호출
- 파라미터로 실제 들어가야 하는 객체인 Member 객체를 Mockito.any()로 감싸서 입력
- .willReturn(member)
- createMember() 메서드가 리턴할 stub 데이터를 넣는다.
- 실행을 해보면 데이터 액세스 쪽 로직이 실행되지 않는다.
- Mockito가 다른 계층의 연동을 끊는다.
Service 계층의 메서드에서 Mockito 적용
- 일반적으로 비즈니스 로직에 대한 테스트는 데이터 액세스 계층과 무관하게 spring의 도움 없이 빠르게 진행 가능해야 한다.
- 예시
- MemberService에 있는 createMember()를 테스트할 때 그 안에 있는 memberRepository.findByEmail(email)에 대해서 데이터 액세스 계층을 접근하지 않고 테스트하는 방법
- Junit에서 Mockito 기능을 사용하기 위해 @ExtendWith(MockitoExtension.class)를 사용
- @Mock를 붙여 MemberRepository 생성
- MemberService 객체를 필드로 선언하여 @InjectMocks를 통해 @Mock을 선언한 MemberRepository를 주입한다.
- member 객체를 하나 생성한다.
- given(memberRepository.findByEmail(member.getEmail))을 통해 해당 email을 가진 객체가 잇는지 확인하는 로직을 테스트한다.
- .willReturn(Optional.of(member))으로 항상 같은 member를 반환하도록 설정
- Mockito.anyString()을 넣어도 작동한다.
- assertThrows(BusinessLogicException.class, ()→memberService.createMember(member))
- memberService의 createMember를 실행하였을 때 memberRepository.findByEmail을 실행하게 되고 해당 email을 가진 member를 반환하도록 설정하였으므로 오류를 반환하기 때문에 테스트 pass한다.
TDD 방식
TDD(Test Driven Development)
- 테스트 주도 개발은 테스트를 먼저 하고 구현을 하는 방식
전통적인 개발 방식
- 서비스를 개발하는 절차
- 서비스 제작 관계자(개발자, 기획자, 웹 디자이너)가 모여 콘셉트와 요구사항 수집
- 서비스 화면을 제공하는 UI를 설계하며 구체적인 기능 요구 사항 정의
- front-end는 UI를 통해 개발, 웹 디자이너 화면 디자인, back-end는 기능에 맞는 백엔드 개발
- 애자일 방식
- 1-3주 동안 기획, 설계, 구현을 반복 진행하여 애플리케이션 완성하는 방식
- 개발자 개발 순서
- 이해 당사자의 요구 사항과 설계된 화면 기반으로 도메인 모델 도출
- 도출된 도메인 모델에서 엔드포인트, 비즈니스 로직, 데이터 액세스를 위한 클래스, 인터페이스 설계
- 클래스 설계 후 큰 틀의 클래스와 인터페이스 구현
- 클래스, 인터페이스 내에 메서드 정의
- 해당 메서드 기능 구현이 잘 작동하는지 테스트
- 테스트 문제가 발생하면 디버깅으로 문제 원인 소명
TDD 방식 개발
- 기본적인 방식
- 모든 조건을 만족하는 조건을 패스하게 작성
- 실패하는 케이스를 작성하면서 수정하여 완성
- 로그인 인증을 위한 패스워드의 유효성 검증
- 길이 8-20
- 알파벳 소문자, 대문자 , 숫자, 특수 문자
- 유효성 검증 테스트 클래스 이름과 메서드 선정하고 작성
- 유효성에 맞는 문자열을 작성하고 PasswordValidator라는 유효성 검증하는 클래스를 선언하고 메서드를 적절하게 작성한다.
- Executable 객체를 사용하여 유효성 검증을 실행하고 결과를 할당
- assertDoesNotThrow(executable)를 통해 executable 객체가 예외 발생하는지 검증
- 특수 문자가 빠졌을 경우의 테스트도 작성한다.
- PasswordValidator에 test case를 모두 실패하는 Exception을 발생하는 것을 작성
- 특수문자를 포함하는지 확인하는 로직을 작성하고 이를 위반할 경우 Exception 발생 작성
- 람다식(특수문자 있는지 확인)
- boolean containSpecialCharacter = password.chars().anyMatch(c-> !isDigit(c) && !isAlphabetic(c));
- 정규 표현식(특수 문자 있는지 확인)
- Pattern.*matches*("(?=.*\\\\W)(?=\\\\S+$).+", password)
- (?=.*\\\\W)
- ?=는 전방탐색, .(dot)은 단일 문자, *(그 전 문자 0 이상 반복), \W는 숫자, 알파벳, _(under bar)가 아닌 문자
- 특수문자인 경우를 찾는 표현
- (?=\\\\S+$)
- \S는 공백이 아닌 문자 ,+는 이전 문자 1 이상 반복, $는 문자열 마지막
- 공백이 아닌 문자가 1개 이상 마지막에 존재
- .+
- 단일 문자 1개 이상이어야 한다.
- (?=.*\\\\W)
- Pattern.*matches*("(?=.*\\\\W)(?=\\\\S+$).+", password)
- PasswordValidator에 로직을 작성하고 특정 부분이 포함되지 않으면 예외를 발생하는 케이스를 테스트에서 확인
- 다른 부분들도 반복해서 진행
TDD 장단점
- 장점
- 테스트 통과할 만큼의 기능으로 기능이 과다하지 않다.
- 단순한 검증에서 확장해 가기 때문에 검증을 잊지 않고 수행
- 리팩터링 할 부분이 보이면 바로 진행하기 때문에 효율적
- 기존 코드를 수정하더라도 테스트 케이스가 존재하여 오류 발생 부담이 적다.
- 리팩터링을 지속적으로 하기 때문에 코드 품질 유지 가능
- 코드 수정 후 바로 테스트 가능하여 수정 결과를 빠르게 확인 가능
- 단점
- 개발 방식에 익숙하지 않다.
- 팀 단위로 개발하므로 팀원 간 사전 협의 필수
'Backend boot camp > Session3' 카테고리의 다른 글
[Spring MVC] 빌드/실행/배포 (0) | 2022.11.11 |
---|---|
[Spring MVC] API문서화 (0) | 2022.11.11 |
[Spring MVC] 트랜잭션 (0) | 2022.11.07 |
[Spring MVC] JPA 데이터 액세스 계층 (0) | 2022.11.07 |
[Spring MVC] JDBC DB Access Layer (0) | 2022.10.29 |