최범균 "테스트 주도 개발"

minseok·2023년 9월 9일
0
post-thumbnail

왜 TDD를 학습하는가?

"전 회사의 서비스 개발에 있어서 급격한 정책, 기획 변경으로 리팩토링에 대한 스트레스가 너무 많았다."

일단 회사 업무를 하면서 아래의 알고리즘이 사용당했다(?)

... 아무튼

통과된 기획의 변경에는 2가지가 있는 것 같다.

1. 목표 자체가 변경
2. 목표는 동일하나 이해관계자에 의해 세부적인 구현이 변경

기획의 변경에는 2개의 성격이 존재한다고 도출을 하였고 2개 다 대응을 할 수 있는 능력을 기르면 좋겠지만 1번 항목을 확실히 대응하려면 테스트 코드 작성 유무를 넘어 유연한 API 설계 경험이 있어야 대응이 가능하다고 생각이 든다.

테스트 코드는 "기능이 올바르게 동작하는지"를 검사하는 것이다.
기능 자체가 180도 변경되면 테스트 코드도 고쳐야한다.


2번 항목은 테스트 코드가 잘 해결해줄 수 있을 것 같다.
만약 "사과"를 달라는 사용자의 요청이 있었다면 그 다음날
"유통기한이 지난 사과", "냄새가 나는 사과"... 같이 일정 조건에 부합하는 사과만 달라는 요청도 꽤 많았다.

늘 목표 자체가 아예 초기화되는 것은 아니니깐 "급격한 정책, 기획 변경으로의 스트레스"문제 해결에 도움을 줄 것이다.


TDD가 당장에 모든 문제를 해결할 수는 없지만 꾸준하게 적용하면 누적되는 스트레스가 천천히 줄어들 것이라고 생각이 든다.





아래에서 부터는 실제 책을 읽고 정리한 내용입니다.
타인을 위해서 쓴 글은 아니고 어차피 학습하니깐 신체의 다양한 기관(?)을 적극 활용하기 위해 작성했습니다 ㅎㅎ


과정

1) 원하는 기능이 정상적으로 수행한다는 것을 증명할 수 있는 테스트를 리스팅한다.

  • '재생버튼을 누르면 노래가 나오는 기계'정도만 적어도 될 것 같다. 이에 대한 테스트 코드는 2.1에서 작성

2) 1번과정에서 도출한 테스트를 작성한다.

2.1 - 실패하는 테스트로 만든다. (Red)

  • 처음에는 컴파일조차 안되니 Stub같은 개념을 활용하자.

2.2 - 일단 빨리 '성공'하게 만든다.(Green)

  • 몇 분 정도 걸릴 것 같으면 일단 중단하고 원래 문제로 돌아가기

2.3 - 올바르게 만들기(리팩토링)

  • 오퍼레이션은 변하지 않고 내부만 수정, 중복을 제거하는 과정이며 의존성으로 인한 단점이 사라지게 된다.
  • 상수같은 것을 변수로 변환하는 과정

2.4 - 2번과정에서 나온 예기치 못한 문제들은 리스트에 계속 기입한다. (객체의 상태에 어울리지 않는 타입을 사용...)


작성 요령

쉬운 경우에서 어려운 경우로 진행한다.

  • 간단하고 다른 여러가지 조건에 영향을 받지않는 것 부터
  • 수 분에서 십여 분 이내에 구현 완료 후 테스트 통과시킬 수 있는 것 부터

예외적인 경우에서 정상인 경우로 진행한다.

  • 예외 상황을 전혀 고려하지 않은 코드에 예외 상황을 반영하려면 코드를 뒤집거나 조건문을 중복해서 추가하는 일이 생긴다.

완급 조절

  • 테스트 통과를 상수를 사용하고 다음 차례에 상수를 제거하고 일반화 시키기

    처음에는 완급 조절이 지루하지만 이러한 연습 과정이 나중에 만들어야 할 코드가 잘 떠오르지 않을 때 점진적으로 구현을 진행할 수 있는 밑거름이 된다.

리팩토링

  • 상수를 변수로 변경하거나 변수 이름 변경은 바로 실행하며 메서드 추출과 같은 구조에 영향을 주는 것은 비즈니스 요구가 더 많아지고 수행하기
  • 발생하지도 않았는데 단정지어서 리팩토링하지 않기
  • 기계적으로 테스트코드 중복을 제거하지 않는다.

TDD, 기능 명세, 설계

기능 명세

우리가 코드를 작성하는 이유는 사용자가 사용할 어떤 기능을 제공하기 위함
기능에 대한 명세는 다양한 형태로 존재하나 중요한 것은 입력출력이 존재한다는 것이다.

로그인 기능을 보겠다.

입력 : 아이디와 암호
결과: 아이디와 암호가 일치하면 성공, 일치하지 않으면 실패

결과에는 익셉션, 변경도 포함된다.
변경의 경우에는 변경 대상에 접근해서 결과를 확인해야 한다.


설계 과정을 지원하는 TDD

설계는 기능 명세로부터 시작한다.
다양한 요구사항 문서를 이용해 기능 명세를 구체화한다.
기능 명세 구체화를 통해 입력, 결과를 도출 하고 코드에 반영을 한다.
...

TDD는 테스트를 만드는 것부터 시작하며 테스트 통과를 위해 코드를 구현하고 리팩토링하는 과정을 반복한다.
중요한 것은 테스트 코드를 먼저 만드는 것이다.
테스트 코드를 먼저 만들기 위해 필요한 것

  • 테스트할 기능을 실행
  • 실행 결과를 검증

필요한 만큼 설계하기

TDD는 테스트를 통과할 만큼만 작성한다.
필요할 것으로 예측해서 미리 코드를 만들지 않는다.(설계에도 동일하게 적용)
필요한 시점에 기능을 만든다.

TDD로 개발하는 코드 비율이 높아질수록 지금 시점에 필요한 설계만 코드에 반영할 가능성이 커진다.

TDD는 미리 앞서서 코드를 만들지 앞으므로 변경된다고 판단하는 요구사항을 반영된 코드가 존재하지 않는다.


기능 명세 구체화

테스트 코드를 작성하기 위해 개발자는 기능 명세를 정리해야 한다.
테스트 코드를 작성하려면 파라미터와 결과 값을 정해야 하므로 요구사항 문서에서 입력과 결과를 도출해야 한다.
테스트 사례 추가 과정에서 구현하기 애매한 점들을 발견하게 된다.

이러한 경우 기획자나 실무 담당자와 상황에 따라 어떻게 동작하는지 구체적으로 정리해야 한다.

보드게임도 처음 규치을 들었다고 해서 바로 완벽하게 이해하는 것은 아님
실제 게임을 진행하면서 다양한 상황에 규칙을 적용하다보면 점차 이해가 간다.
TDD도 마찬가지

구현하는 것은 결국 개발자이다. 최대한 예외적인 상황이나 복잡한 상황을 담당자, 기획자와 이야기해서 끄집어내자

테스트 코드 구성

  • 어떤 상황이 실행 결과에 영향을 줄 수 있는지 찾기 위해 노력하자.
    모든 케이스를 찾을수는 없지만 소프트웨어 품질에 도움이 된다.
  • given, when, then이 테스트 코드에 도움은 주지만 집착하지는 말자
    테스트 코드를 보고 내용을 이해할 수 있으면 된다.

외부 상황과 외부 결과

  • 상황 설정이 테스트 대상으로 국한된 것이 아니고 외부 요인도 존재한다.
@Test
void noDataFile_Then_Exception() {
	File dataFile = new File("badpath.txt");
    assertThrows(IllegalArgumentException.class,
    	() -> MathUtils.sum(dataFile);
    )
}

해당 경로에 파일이 존재하는지 검사해서 삭제하는 방식으로 동작을 보장할 수 있다.

private void giveneNoFile(String path) {
	File file = new File(path);
    if(file.exists()) {
    	boolean deleted = file.delete();
        if(!deleted)
        	throw new RuntimeException("fail givenNoFile: " + path);
    }
}

외부 상태가 테스트 결과에 영향을 주지 않게 하기

  • DB의 데이터를 참조해서 테스트하는 경우 실제 DB에 데이터가 이미 존재하거나 없거나 하는 경우에 따라 테스트가 영향을 받는다.
    테스트 실행 전에 외부를 원하는 상태로 만들거나 테스트 실행 후에 외부 상태를 원래대로 되돌려 놓아야 한다.

외부 상태와 테스트 어려움

  • File, DBMS, 외부 서버같이 다양하다.
    이들 외부 환경을 테스트에 맞게 구성하는 것이 항상 가능한 것은 아니다.
    실행 결과가 외부 시스템에 기록되는 경우도 존재한다.

이러한 경우 테스트 대상을 대역으로 사용하면 편하다.



대역

테스트를 작성하면 외부 요인이 필요한 시점이 존재

  • 테스트 대상에서 파일 시스템 사용
  • 테스트 대상에서 DB로부터 데이터를 조회하거나 데이터를 추가
  • 테스트 대상에서 외부의 HTTP 서버와 통신

이런 외부 요인에 의존하면 테스트가 어려워진다.
(외부 API 서버의 장애, 내부 DB도 상황에 맞게 데이터를 구성하는 것이 항상 가능한 것은 아님)

만약 외부의 카드 정보 검사 API를 사용하고 해당 업체에서 제공한 카드번호의 유효기간이 한 달 뒤일 수 있다. -> 한 달 뒤 테스트는 실패한다.

test double이란 표현은 대역을 의미한다.
대역의 종류에도 stub, fake, spy, mock이 존재


stub

public class StubCardNumberValidator extends CardNumberValidator {
	private String invalidNo;
    
    public void setInvalidNo(String invalidNo) {
    	this.invalidNo = invalidNo;
    }
    
    @Override
    public CardValidity validate(String cardNumber) {
    	if(invalidNo != null && invalidNo.equals(cardNumber)) {
        	return CardValidity.INVALID;
        }
        return CardValidity.INVALID;
    }
}

실제 카드번호 검증 기능(validate)을 구현하지 않고 단순한 구현으로 대체한다.

...
@BeforeEach
void setUp() {
	stubValidator = new StubCardValidtor();
    register = AutoDebitRegister(stubValidator, ...);
    ...
}

@Test
void invalidCard() {
	stubValidator.setInvalidNo("111122223333");
    ...
    register.register(req);
}

register 객체는 register 기능에서 validator를 사용한다.
내부에서는 stub을 사용한다.

도난카드에 대해서도 똑같이 대역을 사용할 수 있다.

public class StubCardNumberValidator extends CardNumberValidator {
	private String invalidNo;
    private String theftNo; // 도난 카드
    
    public void setTheftNo(String theftNo) {
    	this.theftNo = theftNo;
    }
    ...

DB 연동도 대역을 사용하기 적합하다.

public interface AutoDebitInfoRepository {
	void save(AutoDebitInfo info);
    AutoDebitInfo findOne(String userId);
}

대역 구현

public class MemoryAutoDebitInfoRepository implements AutoDebitInfoRepository {
	private Map<String, AutoDebitInfo> infos = new HashMap<>();
    
    @Override
    public void save(AutoDebitInfo info) {
    	infos.put(info.getUserId(), info);
    }
    
    @Override
    public AutoDebitInfo findOne(String userId) {
    	return infos.get(userId);
    }
}

...

private MemoryAutoDebitInfoRepository repository;

...

@Test
void alreadyRegistered_InfoUpdated() {
	repository.save(
    	new AutoDebitInfo("user1", "124124", LocalDateTime.now());
    );		
    ...
    AutoDebitInfo saved = repository.findOne("user1");
}

실제 DB와 커넥션을 맺지도 않고 IO도 발생하지 않아 속도가 빠르다.
하지만 테스트 관련 강의마다 DB 대역을 쓰지않는 케이스도 존재함





대역의 종류

스텁(Stub) - 구현을 단순한 것으로 대체, 단순히 원하는 동작을 수행
가짜(Fake) - 제품에 적합하지는 않지만 실제 동작하는 구현을 제공, DB 대신 메모리를 이용한 구현
스파이(Spy) - 호출된 내역을 기록, 기록한 내용은 테스트 결과를 검증할 때 사용(스텁이기도 함)
모의(Mock) - 기대한 대로 상호작용하는지 행위를 검증, 기대한 대로 동작하지 않으면 예외 발생(스텁, 스파이이기도 함)

구현 전 모든 기능을 설계하는 것은 불가능
개발을 진행하는 동안에도 요구사항이 계속 변경된다.
그럼에도 고민하는 것은 의존 대상을 도출할 때 도움이 된다.


테스트가 어려운 코드

  • 하드 코딩 경로
    경로, 번호, 값은 동적으로 받을 수 있어야 좋다.
    윈도우 기준 파일 경로를 참조하는데 사용 환경이 다른 OS로 변경될 수도 있다.
    -> 생성자, 메서드 파라미터로 받기

  • 의존 객체를 직접 생성
    'DI'기능을 사용하는 경우 의존 객체를 test double로 변경하기 용이하다.
    하지만 직접 제어를 하는 경우 필요한 모든 환경을 구성해야 한다.
    -> 의존 대상 주입 받기, 생성자가 없는 레거시 코드의 경우 Setter를 사용해서 주입

  • 실행 시점에 따라 달라지는 결과
    LocalDateTime.now()을 테스트 기능 내부에서 직접 호출하는 경우
    동일한 요청에도 결과가 달라진다. (ex Random)
    -> 이런 값들도 내부에서 처리하지 않고 주입받는 방향으로 사용

  • 역할이 섞여 있는 코드
    의존 객체가 섞여있거나 여러 코드가 섞여있다면 테스트를 하고 싶은 코드만 따로 분리한다.

  • 테스트 대상의 소스 소유 X + 정적 메서드 사용, final 사용
    외부 라이브러리가 정적 메서드를 제공하는 경우, 대체할 수 없다.
    이 경우 한 번 감싸서 사용한다.

추가적으로..

  • 메서드 중간에 소켓 통신이 포함
  • 테스트 대상이 사용하는 의존 대상 클래스나 메서드가 final

How to test an application built on non-testable code?
https://medium.com/xsolve-blog/how-to-test-an-application-built-on-non-testable-code-961b24ff35ff




테스트 범위와 종류

  • 기능 테스트
    (웹, 모바일 ~ 서버 ~ DB, 외부 서비스)를 엮어서 한 번에 진행한다.
    끝에서 끝까지 올바른지 검사하기 때문에 E2E Test로도 볼 수 있다.

  • 통합 테스트
    시스템의 각 구성 요소가 올바르게 연동되는지 확인한다.
    기능 테스트는 사용자 입장에서 테스트하지만 통합 테스트는 소프트웨어의 코드를 직접 테스트한다.(프레임워크, 라이브러리, DB, 구현 코드)

  • 단위 테스트
    개별 코드나 컴포넌트가 기대한대로 동작하는지 확인한다.(클래스, 메서드)
    일부 의존 대상은 모킹하기도 함

기능, 통합 테스트를 하기위해서는 DB 연결, 컨테이너 초기화, 웹 서버 구동, 앱 설치 같이 속도를 느리게 하는 요인이 많음.
반면 단위 테스트는 테스트 코드만 준비하면 끝난다.

단위 테스트는 통합 테스트로 만들기 힘든 상황을 쉽게 구현할 수 있다.
가능하면 다양한 상황을 단위 테스트에서 다루고 기능 테스트는 주요 상황에 초점을 맞춰야 함
(단위 테스트만으로 정확히 동작한다고 보장하기 힘들다.)




테스트 코드와 유지 보수

테스트 코드도 그자체로 코드이기 때문에 제품 코드와 동일하게 유지보수 대상이다.
테스트 코드를 유지보수하는데 시간이 많이 들기 시작하면 점점 테스트 코드를 손보지 않음.
발생되는 문제

  • 실패한 테스트가 새로 발생해도 무감각
  • 빌드 통과를 위해 실패한 테스트를 주석으로 처리, 고치지도 않음

테스트를 만들지 않으면 테스트가 가능하지 않은 코드를 만들게 되고 이는 다시 테스트 코드를 작성하지 않음 (악순환)

결론적으로 테스트 코드자체의 유지보수성이 좋아야 함



유지보수성을 높이기 위한 요소


1. 변수나 필드를 사용해서 기댓값 표현하지 않기

  • 변수에 할당된 값을 참조해서 사용하는 경우
String month = "1945";
...
긴~ 코드
...

assert month == expectedMonth

위의 테스트가 실패하는 경우 month의 값을 찾으러 편집창을 왔다 갔다 해야 함


2. 두 개 이상 검증하지 않기

	@Display("같은 ID가 없으면 가입에 성공하고 메일을 전송")
	@Test
    void registerAndSendMail() {
    	// 1. 회원 데이터가 올바르게 저장되었는지 검증
        .. 블라 블라 ..
        [
        aasert id == savedUser.getId();
        assert email == savedUser.getEmail();
        ] <- 한 세트
        
        // 2. 이메일 발송을 요청했는지 검증
        .. 블라 블라 ..
        [
        assert "email@email.com" == realEmail
        ] <- 한 세트
    }

잘못된 테스트는 아니지만 한 테스트에서 검증하는 내용이 두 개 이상이면 테스트 결과 확인 집중도 가 떨어진다.
첫 번째 테스트가 통과해야 두 번째 테스트의 성공 여부를 알 수 있다.

assert가 검증의 기준이 아니라 문맥이 기준이다.


3. 정확하게 일치하는 값으로 모의 객체 설정하지 않기

@DisplayName("약한 암호면 가입 실패")
@Test
void weakPassword() {
	BDDMockito.given(mockPasswordChecker.checkPasswordWeak("pw"))
    .willReturn(true);
    
    assertThrows(WeakPasswordException.class, () -> {
    	userRegister.register("id", "pw", "email");
    });
}

이코드는 작은 변화에도 실패한다.
"pw"인 경우만 통과하므로 유지보수성이 낮다.

이 테스트는 UserRegister가 원하는대로 동작하는지 확인하는 테스트이며
"pw"가 약한 암호인지 확인하는 테스트가 아니다.


@DisplayName("약한 암호면 가입 실패")
@Test
void weakPassword() {
	BDDMockito.given(mockPasswordChecker.checkPasswordWeak(Mockti.anyString()))
    .willReturn(true);
    
    assertThrows(WeakPasswordException.class, () -> {
    	userRegister.register("id", "pw", "email");
    });
}

모의 객체는 가능한 범용적인 값을 사용해야 한다. ("pw"같이 특정한 값 X)
이렇게 하면 약간의 코드 수정 때문에 테스트가 실패하는 것을 방지할 수 있다.

암호가 약하다는 것을 가정하고 register 동작이 실패하는 것을 검증하는 것 같다.


4. 과도하게 구현 검증하지 않기

	@DisplayName("회원 가입시 암호 검사 수행")
    @Test
    void checkPassword() {
    	userRegister.register("id", "pw", "email");
        
        // checkPasswordWeak() 호출 여부 검사
        BDDMockito.then(mockPasswrodChecker)
        .should()
        .checkPasswordWeak(Mockito.anyString());
        
        // findById() 메서드를 호출하지 않는 것을 검사
        BDDMockito.then(mockRepository)
        	.should(Mockito.never())
            .findById(Mocktio.anyString());
    }

register() 기능의 내부 구현을 검증하는 코드이다.
나쁜 것은 아니지만 조금만 변경해도 테스트가 깨질 가능성이 높다.

테스트 코드 내부 구현보다는 실행 결과를 검증해야 한다.(반환 값)

하지만 이미 존재하는 코드에 테스트 코드를 작성하는 경우 내부 구현을 검증할 수도 있다.
이러한 경우 점진적으로 리팩토링해서 내부 구현이 아닌 결과를 검증하도록 하자.


5. 셋업을 이용해서 중복된 상황 설정하지 않기

전체적인 코드의 길이가 짧아지나 모든 테스트들이 동일한 자원을 바라보기 때문에
조금만 변경해도 테스트가 깨질 수 있다.
테스트가 실패할 시 분석을 위해 @BeforeEach 메서드와 @Test 메서드를 이동하면서 확인해야 한다.
처음에는 셋업으로 중복 상황을 제거하면 쉬울 수 있으나 시간이 지나면 방해 요소가 된다.

테스트 메서드는 검증을 목표로 하는 하나의 완전한 프로그램이여야 한다.


6. 통합 테스트에서 데이터 공유 주의하기

통합 테스트를 위해서는 DB데이터를 알맞게 구성해야 한다.
이를 위해 @Sql("classpath:...sql") 같은 기능을 사용해 많은 테스트가 동일한 DB데이터 셋업을 가지게 한다.
5번과 같은 맥락이며 쿼리 파일을 조금만 변경해도 모든 테스트에 이펙트가 간다.

코드 데이터같이 모든 테스트가 같은 값을 사용하는 데이터는 문제가 없으나
테스트 메서드에만 필요한 데이터인 경우에 각 각의 테스트 메서드에서 초기화 하도록 한다.


7. 통합 테스트의 상황 설정을 위한 보조 클래스 사용하기

6번 항목의 단점을 위해 테스트 메서드에서 직접 상황을 구성함으로써 테스트 분석에는 용이하지만 반대로 상황을 만들기 위해 여러 테스트 코드에 중복된다.
이러한 경우 상황 설정을 위한 보조 클래스를 사용한다.

public class UserGivenHelper {
	public void givenUser(String id, String pw, String email) {
		...
	}
}

public TestClass {
  @Test
  void dupId() {
      userGivenHelper.givenUser(...);
	}
}

음.. 6번 항목하고 뭐가 다른거지..?


8. 실행 환경이 다르다고 실패하지 않기

같은 테스트 메서드가 실행 환경에 따라 성공하거나 실패하면 안 된다.

public class BulkLoaderTest {
	private String bulkFilePath = "d:\\mywork\\temp\\bulk.txt";
    
    ...
}

Mac OS에서는 D 드라이브가 없어 실패할 수 있다.
Window OS라도 역시 D 드라이브가 없으면 실패한다.

테스트에서 사용하는 파일은 프로젝트 폴더를 기준으로 상대 경로를 사용하는 것이 좋다.

public class BulkLoaderTest {
	private String bulkFilePath = "src/test/resources/bulk.txt";
}

테스트 코드에서 파일을 생성하는 경우에도 OS가 제공하는 임시 폴더를 사용하거나 메이븐 프로젝트라면 target 폴더에 결과를 저장하도록 한다.

Junit은 특정 OS에서만 실행하게 해주는 기능도 제공한다.


9. 실행 시점이 다르다고 실패하지 않기

public class Member {
	private LocalDateTime expiryDate;
    
    public boolean isExpired() {
    	return expiryDate.isBefore(
        	LocalDateTime.now() <- 동일한 인풋에 다른 아웃풋을 유발함
        );
    }
}

로직에 참조되는 모든 것은 인풋으로 받는다.
public class Member {
	private LocalDateTime expiryDate;
    
    public boolean isExpired(LocalDateTime now) {
    	return expiryDate.isBefore(now); <- 동일한 인풋에는 동일한 아웃풋을 보여줌
    }
}

10. 랜덤하게 실패하지 않기

실행 시점에 따라 테스트가 실패하는 또 다른 예는 랜덤 값 이용이다.

public class Game {
	private int[] nums;
    
    public Game() {
    	Random random = new Random();
        int firstNo = random.nextInt(10);
        ...
        this.nums = new int[] { firstNo, secondNo, thirdNo };
    }
    
    public Score guess(int ...answer) {
    ...
    }
}

만약 아무것도 일치하지 않는 경우를 테스트하려고 해도 작성할 수 없다.

// Game Class의 생성자
public Game(int[] nums) { <- 직접 기준이되는 값을 넣어준다.
	this.nums = nums;
}
// Game Class의 생성자
public Game(GameNumGen gen) { <- 혹은 랜던 값 생성을 다른 객체에 위임한다.
	this.nums = gen.generate();
}

11. 단위 테스트를 위한 객체 생성 보조 클래스

단위 테스트를 위해 복잡한 상황 구성이 필요할 때가 있다.

예를 들어 "설문에 답변하는 기능"을 구현 하려면
1. 설문이 공개된 상태
2. 설문 조사 기간이 끝나지 않음
3. 설문 객관식 문항이 2개
4. 각 객관식 문항의 보기가 2개

이러한 경우를 위해 객체 생성 클래스를 따로 만들어 복잡함을 줄일 수 있다.
정적팩토리를 사용하고 조금 더 여러 경우 "기간이 지난 설문", "아직 시작하지 않은 설문"이 필요한 경우 빌더패턴도 고려할 수 있다.


12. 조건부로 검증하지 않기

테스트는 성공하거나 실패해야 한다.
조건에 따라서 단언을 하지 않으면 그 테스트는 성공하지도 실패하지도 않는 테스트가 된다.

번역기(Translator)가 "cat"을 번역하지 못해도 이테스트는 통과한다.

@Test
void canTranslateBasicWord() {
	Translator tr = new Translator();
    if( tr.contains("cat")) {
    	assertEqauls("고양이", tr.translate("cat"));
    }
}

만약 실수로 items.size()의 값이 0이 나오면 이 테스트는 통과한 것처럼 보인다.

@Test
void firstShouldBeAdminItem() {
	...
    if(items.size() > 0) {
    	assert ADMIN == items.get(0).getType();
    }
}

고양이 번역 테스트를 아래같이 수정한다.

assert tr.contains("cat");
assert "고양이" == tr.translate("cat")

아이템 사이즈도 아래같이 수정한다.

assert itmes.size() > 0;
assert ADMIN == items.get(0).getType();

13. 통합 테스트는 필요하지 않은 범위까지 연동하지 않기

@SpringBootTest
public class MemeberDaoIntTest {
	@Autowired
    MemberDao dao;
    
    @Test
    void findAll() {
    	List<Member> members = dao.selectAll();
        assert members.size() > 0;
    }
}

테스트 대상은 DB와 연동을 처리하는 MemeberDao이지만 @SpringBootTest를 사용하면
모든 스프링 빈이 초기화된다.

@JdbcTest
@AutoConfigureTestDatabase(repalce = Auto...Replace.NONE)
public class MemberDaoIntJdbcTest {
	...
}

@JdbcTest를 사용함으로 DataSource, JdbcTemplate 등 DB 연동과 관련된 설정만 초기화한다.
스프링 빈을 모두 초기화하는 것보다 속도가 더욱 빠르며
DataSource와 JdbcTemplate을 테스트 코드에서 직접 생성하면 테스트 시간은 더 빨라질 것이다.


profile
즐겁게 개발하기

0개의 댓글