[1주차] TDD 학습 정리
Test 가 중요한 이유
- 기능개발 필요 시 -> 어떤 테스트의 통과여부가 기능에 대한 검증 역할을 할수 있을지 생각 필요
- 초기 시 기능 먼저 개발 -> 내가 어떤 테스트를 먼저 짜야할지 항상 생각 -> 나중에 추가적으로 채울수있음
- TDD 실력이 채워진 후 테스트 작성 -> 기능 개발
테스트 종류
- End-to-end
- 대상 : 전체 애플리케이션의 흐름
- 목적 : 애플리케이션이 제공하는 기능을 사용자 시나리오 기반으로 문제 없는지 점검
- 내가 정의한 흐름의 처음부터 끝까지를 테스트하는 것
- integration testing(통합 테스트)
- 대상 : 서로 다른 module / system 의 상호작용
- 목적 : 맞물려 돌아가는 기능이 모여 정상적으로 원하는 기능을 제공하는지 점검
- 예시 : 각 손가락이 꺽이는지는 이미 유닛 테스트 진행완료됨에 따라 손가락이 유기적으로 움직이는지만 테스트
- 보통 단위테스트에서 모두 테스트되며, 인풋 아웃풋만 테스트(블랙 박스테스트)
- 현재 정의로는 통합테스트 = @SpringBootTest 사용한다.
- unit testing(단위 테스트)
- 대상 : 단일 기능 혹은 작은 단위의 함수/객체 등 (독립적인 객체에 대한 테스트)
- 목적 : 가벼운 비용으로 새로운 기능 혹은 개선이 기존의 rule 을 위배하지 않는지 점검
- 예시 : 각 손가락이 꺽이는지는 유닛 테스트
- 단위테스트를 모두 작성하며, 화이트 박스 테스트
- A service 만 테스트함(B service에 의존성이있다면 지워줄꺼다. )
Test Double
- 테스트 더블은 실제 컴포넌트를 대체할 수 있도록 하는 대역이다
- mock, stub은 결국 다른 객체와 의존성을 지우기위해 사용한다
- 실제 컴포넌트에 대해 행동을 모방하고, 이를 통해 기존의 강한 결합도를 낮추고 테스트 중 제어 가능하도록 한다.
Mock
- 테스트를 위해 특정 기능에 대해 정해진 응답을 제공하는 객체
- 입력과 상관없이 어떤 행동을 할 지에 초점을 맞춘 객체
(=”아 모르겠고~” 정해진 응답제공
(=그니까 대충대충 뭐가오든 반환해, 지금 테스트할때 mock처리대상에 상태가 중요한게아니야) - Stub보다 넓은 개념
- Mock Library 를 통해 특정 행동에 대한 출력을 정의
Stub
- 테스트에 필요한 호출에 대해 미리 준비된 응답을 제공하는 객체
- 입력에 대해 어떤 상태를 반영하는 지에 초점을 맞춘 객체
- Interface 기반으로 테스트에서 보고자하는 (혹은 필요로 하는) 구현에 집중한 구현체를 정의
- 이런 숫자(1L, 2L)가 들어가면 이렇게 각각 대답해줘 => stub 입력을 특정했기 때문에
(중요) 경계값 검증
- 0보다 적거나 같은, 4보다 크보다 같은 등은 0, 4에 대한 근처를 테스트해야함
- 유저 A, 유저 B 에 대한 테스트같은 경우 -> mock 아닌 stub이 필요함.(인풋에 따라 아웃풋이 바뀌는 어떤 상태를 반환해야하므로)
테스트 작성 시 주의사항
- TC( =TestCase) 를 “뾰족하게” 뽑아서 기능에 “예민하게” 반응하는 테스트를 작성했는가?
- 테스트 커버리지 100% 가 아니라, 정확히 기능의 동작을 확인하는 테스트를 작성해 주세요.
- 어떤 테스트 명세 작성해서 정확한 기능 동작을 검증 할수있을까 고민필요(=뾰족한 테스트)
- 주요 기능에서
private
접근자, 객체 간의 강결합 같이 테스트 불가능한 코드는 가능한 한 지양하는 것이 좋습니다.private
->public
대신 역할 분리를 하는 것도 고려해야함- 단순히 클래스에서 함수분리한다고 private으로 바꾸면, 해당 책임을 검증하기 위해 결국 통합 테스트로 밖에 테스트 못한다 -> 이는 결국 통합테스트에서 모든 객체들이 유기적으로 움직이는지 모두 알고있어야 통합테스트가 가능함.
- private 메서드를 다른 메서드에서도 사용하고있다면, 다른 메서드 모두 통합테스트에 동일 케이스 조건을 모두 작성해야함
- 정답을 줫는데 정답을 응답하면 정상이다. 이를 테스트할 필요가 있을까?
- *어? 안되는데 -> 실패케이스를 모두 테스트 한다면, 역으로 정상 기능 동작 증명가능하다. *
- 실패 케이스 테스트는 기본적으로 if ->then으로 작성한다.
- page가 0보다 작으면 검색을 실패(어떤 Exception 동작한다 등 구체화)한다.
- pazeSize가 0보다 작으면 검색을 실패(어떤 Exception 동작한다 등 구체화)한다.
- 실패 케이스 테스트는 기본적으로 if ->then으로 작성한다.
- *어? 안되는데 -> 실패케이스를 모두 테스트 한다면, 역으로 정상 기능 동작 증명가능하다. *
- 객체 또는 함수에 대해서 테스트 작성 시, 함수 또는 객체 하나가 많은 작업에 대한 책임(연관 테스트가 많음) 을 갖고있다면, 책임 분리 해야한다는 신고다.
[1주차] 공개 Q&A 요약
mock 과 stub 차이
stub : 단순 값 반환에 대해서 검증하면서 왜 “상태” 검증이라고 하는가?
객체의 상태 = “반환 값” -> 반환 값에 따라 테스트의 결과가 달라야함을 뜻함.
따라서 상태에 대해서 다른 결과를 테스트로 검증하기때문에 “상태” 검증
When(service.call(1L).willReturn(List(1L, 2L, 3L)))
아래 ARepositoryStub은 상태검증의 Stub
interface ARepository { fun getA(): Long } class ARepositoryStub : ARepository { override fun getA(): Long { return 1L } } <!--0-->
controller layer와 service layer에서 검증(valid) 차이
- (클럽문지기) Controller valid -> 검증의 관심사 숫자야? 컨트롤러에서는 돈은 음수면 안되는데?, 비즈니스에서의 관심사는 아님
- 좌석인지, 입석인지 검증안함. 입장가능한지만 검증
- (클럽 카운터) service -> 이미 타입은 안다. 호출되는거니까. 서비스 입장에서는 돈은 음수면 안되는데?
- 좌석인지, 입석인지, 입장료가 있는지, 이용가능한지까지 검증
- @Valiated는 올바른 값인지만 판단하고보통, 비즈니스 로직과 관련있는경우, 무조건 service 나 도메인 모델이서 검증
동시에 여러번 충전, 여러번 사용에 대해서 어떻게 테스트 -> 통합테스트 추천
- 단위테스트로 진행 시에는 내가 정해놓은 stub의 값을 뱉기때문에 의미가없다.
- 통합테스트시 when완료 후 then 에서 db접근하여 결과값과 예측결화 조회 검증
동시성 이슈가 통합테스트인 이유
- 동시성 이슈가 발생하는지에 대한 테스트 -> 같은 자원에 동시로 접근할때 -> 즉, 진짜 2개는 객체여아함-> 통합테스트이다.
- Tip: “행동의 경계” 객체에서 테스트합니다
- 즉 현재 Service 에서 DB에 접근할때, 동시성이슈가 생기므로, Service 기준으로 테스트 하면된다.
꿀팁) 테스트 패키지 구조
1 | /main/src/point/PointService.java <- 요거 테스트할거임 |
검증의 범위 및 의미
- 비즈니스 로직으로 의미가 있는 것만 테스트하면됨. (테스트 커버리지100%필요없다)
- 단위 테스트 코드를 작성 할때, red-green-refactor(RGB) 사이클을 통해서 작성하는 것이꼭 필요한가? -> 제일 중요한건, TC를 “뾰족하게” 뽑아서 기능에 “예민하게” 반응하는 테스트를 작성했는가 에 대해서 집중하자
1 | 테스트 (주어진 id 의 유저가 없다면, 0원을 가진 유저를 반환한다.) |
작업의 단위 하나 = 트랜잭션
질문
- 포인트 충전시 UserPointTable, PointHistoryTable 테이블에 저장하는데
테스트시 UserPointTable, PointHistoryTable 각각 테이블에 들어간 값을 확인하면 될까요?
혹시 UserPointTable가 들어가고 어떠한 이유로 PointHistoryTable에 들어가지 못했다면,
해당 테스트는 실패라고 봐야하는건지 궁금합니다.
테스트 한 METHOD에서 UserPointTable 테이블에 들어가는지, PointHistoryTable 테이블에 들어가는지 2개이상의 내용을 검증하게되는 것 같아 혼란스러워서요..
답변
당연히 YES.
[내가 검증하고자 하는 기능이 충전할 시, 유저의 돈과 내역은 정상적으로 반영되어야 한다.] 를 검증한다면, 내가 생각하는 “기능의 단위” 가 <”유저의 돈이 변경 반영” + “내역 저장”> <– 작업의 단위 = Transaction
비즈니스 로직이 거의없는 DAO 단순 참조하는 함수의 테스트 여부?
- 통합테스트만 진행.
- 비즈니스로직(자기 고유의 기능)없음 -> 단위 테스트 없음 ->
단위 테스트 대상
유저 포인트 조회 기능
1 |
|
위에 조건에서 유저포인트 조회 함수 테스트
1. 단위테스트 필요한가?
- userId 가 0일 때 예외 발생하는지?
- userId 가 음수일 때 예외 발생하는지?
- userId 가 양수일 때 예외 발생 안하는지?
2. 통합테스트
- 그냥 유저를 조회하면 0원이다.
- 그 유저에 대해 충전하고 조회하면 충전한 금액을 반환해야 한다.
동시성 테스트란 어떻게 짜는가?
동시성 테스트
원리: 동시에 같은 자원에 접근했을때, 내가 의도한 대로 결과를 갖는가?
동시에 같은 자원에 접근했을 떄, 한 명이 실패하는가?
같이 수정하려고 하면 수정하는 동안 접근한 애는 실패시킬때
- 결과를 조회해서 내 생각과 맞아 떨어지는지
- 한명은 실패 했는지도 검증
동시에 같은 자원에 접근했을 때, 두 요청이 다 반영되어야 하는가?
같은 수정하려면 하면 모든 요청이 줄지어서 성공시킬 때
- 결과를 조회해서 내 생각과 맞아 떨어지는지
- 실패하지않았는지 검증하면 좋다 안해도됨
- 테스트 코드의 원리 -> throw되면 실패 -> 검증할 필요가 없다
동시성 테스트시 라이브러리 참조
CountDownLatch ( 멀티쓰레드 기반의 동기화 도구, 특정 쓰레드가 다른 쓰레드 작업 끝날때까지 기다림)
테스트 쓰레드 에서 멀티 쓰레드로 다양한 행동을 “동시에” 수행시킨다.
다 끝날때까지 기다린다.
그리고 결과를 조회해서 검증한다.
예시코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15int threadCount =3
CountDownLatch latch = new CountDownLatch(threeCount)
for (int i = 0; i < threadCount; i++){
new Thread(() -> {
//동작
latch.countDown(); //latch 값 감소
}).start()
}
latch.await(); //내 모든 쓰레드가 countDown() 시킬때까지 대기 // latch=0이 될때까지 기다림
// latch =0 이 되면 다음 블록 실행
assert() // 검증
- CompletableFuture : 이건 JVM 의 멀티 쓰레드 치트키 인데, 그냥 latch써보기
질문
- 아래보면 PointService 가 두개의 구현체를 각자 받아서 처리하고있는데, 해당 부분을 테스트한다면, 단위테스트와 통합테스트 둘다 해야하는가? 아니면 단순히 mock테스트로 호출여부만 판단하면될까?
- 내가 이해한게맞나?
- service -> service 호출가능, service들을 최대한 쪼개면 테스트 가 용이해 질텐데 맞는 방향인가?
1 |
|
멘토링
단위테스트, 통합테스트
최대한 예외처리, fail처리를 검증하는것으로 단위테스트 처리
성공에 대해서만 통합테스트진행
외부시스템 또는 본인 객체말고의 연동관련에 대해서는 통합테스트 진행해야함. 실패, 예외처리 등등
동시성 이슈 발생하는곳
- 같은 자원에 서로 경쟁하게 되는곳
- 논리적 원자성을 보장해야하는 곳
- @Transactional 사용하는곳