테스트 코드

Dev·2022년 4월 16일
2

테스트 코드란 프로덕트 코드를 테스트(검증)하는 코드다. 작성한 코드가 문제 없는지 확인하는 용도로 사용된다.

1. 테스트 코드 목적

[1] 버그를 잡는다.

  • unit test, integration test 등 다양한 방식의 테스트 코드로 프로덕트 코드를 검증한다. 특정 기능의 테스트 커버리지가 높다면 이 기능은 많은 예외를 검증한 코드라는 '신뢰감'이 생겨, 잠재적 버그가 적다는 것을 알 수 있다.
  • 사람은 기본적으로 내가 어떤 실수를 할지를 모른다. 따라서 프로덕트 코드에서 가정한 부분에 결함이 있다면 테스트 코드에서도 결함이 있기 마련이다. 하지만 테스트 코드가 있다면 프로덕트 코드의 '의도에' 맞게 검증할 수 있어 최소한의 결함을 줄여줄 장점이 있다.

[2] 수동 테스트를 자동화해, 개발 시간을 단축시킬 수도 있다.

  • 예를 들어 신규 기능을 추가하는 과정을 생각해보자. 코드를 수정하고, 서버를 동작시키고, 필요에 따라 DB에 데이터를 입력하고, api를 콜하고, 테스트를 마치면 DB 데이터를 정리하는 과정을 '수동'으로 반복해야한다. 만약 테스트가 한 두번 하면 굳이 테스트 코드를 만들 필요는 없을 수도 있지만, 테스트가 여러번 발생할 경우 귀찮은 작업을 '자동화' 테스트로 대체하면 테스트 시간이 단축될 수 있다.
  • 테스트 코드 작성에 시간이 늘어난다고 느껴지는 이유는 이전에 작성한 테스트 코드 파악이 어럽거나, 테스트 하기 힘든 구조로 프로덕트 코드를 개발을 가능성도 있다. 혹은 당연히 해야할 테스트 과정을 무시한채 넘어가는 경우도 있다. 테스트 코드는 프로그램 구동 후 발생할 에러를 사전에 발견할 수 있는 기회다.
  • 물론 테스트 코드가 있어서, 개발 시간이 항상 단축되는건 아니다. 이전 테스트 코드 파악이 어렵다면 테스트 코드 파악하는데 시간이 너무 오래 걸릴 것 이다. 예를 들어, 기능 하나 수정하는데 프로덕트 코드는 금방 바꾸는데 테스트 코드의 많은 수정이 필요하다면, 체감상 시간이 아까운 느낌이다. 여기서 말하는 시간이 아깝다라는 말은 테스트 자체가 아깝다라는 건 아니다. 내가 고친 로직은 당연히 테스트가 필요하다. 다만 이걸 테스트 코드로 하냐, 내가 직접 테스트를 하냐, 혹은 테스트를 나중에 한꺼번에 한다 요 차이가 있는 것 같다. 다음은 몇가지 테스트 코드 추가에 대한 생각이다.
    • 절대 edge case가 나오면 안되는 도메인에서는 시간이 걸리더라도 엄격히 테스트 코드를 추가하는게 맞다고 생각한다. ex) 금융, 방산, 보안 등. 근데 실 서비스 중에 당연히 버그가 나오면 안되는거 아닌가? 요건 팀의 바쁜 정도, 버그가 너무 리스키한 경우 요런 것들을 모두 고려해보자.
    • 핵심 로직이 담긴 부분에 대해서는, 나중에 유지보수시 실수로 핵심 로직이 깨지는걸 방지하고자 요건 지켜야돼! 요런 것들이 있으면 부분적으로 테스트 코드를 추가하자.
    • 실제 서비스 중 예상치 못한 데이터 인입, 시나리오, 잘못된 함수 사용 등이 있다면 요런 포인트들이 나올 때마다 테스트를 추가하고, 요걸 개발하자.
    • 프로젝트 오픈 전과 같이 기획이 자주 바뀌고 로직이 자주 바뀌는 부분에서는 테스트 코드로 인해 오히려 테스트 코드 때문에 생산성 저하가 일어날 수 있다. 따라서 이렇게 자주 바뀔 수 있는 부분에 대해서는 테스트 코드가 오히려 없는게 나을 수 있다.
    • 그 외에는 테스트 코드를 관성적으로 추가하고싶진 않다.

[3] 리팩토링의 자신감이 생긴다.

  • 리팩토링 작업은 기능은 동일한데, 코드를 정리하는 작업이다. 내부 코드를 변경하기 때문에 코드의 변경이 전체 시스템에 영향이 끼칠지 체크해야한다. 이때 커버리지가 높은 테스트코드가 있다면 리팩토링한 코드가 기존 기능에 영향을 끼쳤는지 알 수 있다. 따라서 리팩토링의 두려움 (기존 기능 영향)이 줄어들 수 있다.

[4] '잘 작성한' 테스트 코드는 문서로서의 역할도 가능하다.

  • 테스트 코드 구조가 특정 패턴으로 팀원들에게 가독성 있다면, 처음 기능을 보는 개발자가 프로덕트 코드 구조로 파악하는 것보다 테스트 코드를 통해 특정 조건에서 어떤 메소드가 이러한 기능을 수행한다는 코드 파악이 수월해질 수도 있다.

2. 테스트 코드 종류

[1] Unit Testing

함수 단위 테스트

  • 함수 단위로 테스트한다. 다른 테스트보다 실행 시간이 짧고, 한번에 많은 기능을 테스트하지 않아 테스트 코드 파악이 수월하다. 따라서 기능의 정상 동작 유무 및 테스트 코드의 버그를 빠르게 잡을 수 있다.
  • 메소드 단위로 테스트하기 때문에 비지니스 중심의 테스트보단 개발 중심의 테스트다. 개발 및 피드백 시간이 빠른 장점은 있지만 서비스 자체를 테스트하진 않기 때문에 다른 테스트보다는 이 프로덕트 코드가 안정적이다라는 확신은 덜하다.
  • 기본적으로 public method 기준으로 테스트가 필요하다고 생각한다. 하지만 간혹 핵심 로직 테스트를 위해 private 메소드도 테스트가 필요할 경우가 있다. 이때 powermock, reflection 방식으로 테스트할 수는 있지만 해당 메소드에 문제가 있을 경우 컴파일단에서 에러를 못잡는 단점이 있다는건 인지해야한다.

[2] Integration Testing

하나의 서비스에서 통합 테스팅

  • 단위 테스트와 달리 외부 라이브러리, 디비, 파일시스템 등 까지 묶어 통합적으로 검증할 때 사용한다. 이는 db에 접근하거나, 더 큰 모듈, 다양한 환경이 제대로 작동하는지 확인하는데 사용된다.
  • 이는 단위테스트보다 확실히 신뢰성이 생긴다. 단위테스트에서 발견하기 어려운 버그를 찾을 수도있다. 예를 들어, 싱글코어에서는 동작하나, 멀티코어에서는 버그가 나오는 등 실제 외부 서비스와 연동하여 연동간의 문제를 발견할 수 있다.
  • 하지만 단위테스트보다 실행시간이 오래걸리고, 에러 발생시 어디서 에러가 나온지 확인이 쉽지 않아 유지보수하기 상대적으로 힘들다.

[3] End To End Testing

MSA 구조에서 연계된 다수의 서비스(모듈)를 통합 테스팅

  • 다른 테스트보다 좀 더 서비스에 가깝게 테스트하여 신뢰성이 생기지만, 피드백이 느리며, 실패를 분석하기 어렵다. 특히 잘게 쪼개진 서비스간 api 통신의 기반을둔 MSA 환경에서 버그를 찾기 까다롭다.
  • MSA 특성상 연계된 많은 서비스들에 영향을 받고, 테스트 중 다른 팀의 서비스 버전이 바뀔 수도 있다. 즉, 외부 서비스들에 영향을 굉장히 많이 받는다. 그렇다고 특정 버전에 맞춰 모든 서비스팀이 함께 배포한다면 MSA 구조가 가진 독립 배포의 장점이 사라지게된다.
  • 이 테스트는 각 서비스간 타협점이 중요하다고 생각한다. 버전을 맞춘다거나, end-to-end 테스트 집합을 공동소유의 코드로 보고 모든 개발 팀에서 체크인하게 해야한다.

[4] 소비자 주도 테스트

  • 기존 기능을 변경할 때 우리 서비스를 사용하는 다른 팀에 영향이 없어야한다. 소비자(다른 팀)의 기대사항(우리팀이 주는 리스폰스)을 코드로 표현하고, 그 응답 값에 부합하지 않는 경우를 (다른팀에 영향이 가는) 거르는 테스트다.
  • 비용이 큰 end-to-end test에서 소비자 주도 테스트 && 통합 && 단위 테스트 방식을 활용하여 테스트 비용을 줄일 수도 있다.
  • 그렇다고 end-to-end test가 완전히 배재된건 아니다. 도메인마다 다른데, 어떤 곳에서는 출시가 오래 걸리더라도 실 환경 배포 이전에 어떤 결함도 없애는 것이 중요할 수 있다. 따라서 도메인에 맞게 적절히 선택하면된다.

[5] 출시 후 테스트

  • 배포 전에 모든 예외를 테스트 하기 힘들다. 사전에 문제를 발견하고 실 환경에서 맞딱뜨릴 문제를 줄이기 위해 많은 테스트 코드를 작성하였는데, 특정 시점부터 테스트를 늘려도 효과는 적은 현상이 발생한다. 즉, 리소스대비 효율이 안나온다. 결국 배포전에 테스팅으로 장애의 가능성을 완전히 없앨 수는 없다. 그래서 배포 후 다양한 방식으로 모니터링하며 로그를 잘 남겨 후속 대응을 대비하자.
  • alpha/stage 단게에서 테스팅, 사용자 트래픽 일부를 신규 버전으로 흘리거나, 일부 유저에게만 신규 버전을 배포하는 방식 등이 있다.
  • 사실 아무리 좋은 테스트 코드라도 QA 1명 배치하는게 훨씬 이득일 수 있다. 사람은 실수할 여지가 너무나도 많고, 개발하는 사람이 테스트하는 건 너무 유토피아인 느낌이다.

3. 좋은 테스트코드란

테스트 코드를 작성할 때 특정 언어나 기술을 사용했다고 좋은 테스트 코드가 아니다. 그 보다는 아래의 특성을 얼마나 가지냐가 중요하다고 생각한다.

[1] 가독성

  • 읽기 어려운 테스트 코드는 무엇을 테스트하는지 파악이어려워 유지보수시 테스트 코드 파악이 힘들고, 코드 리뷰 역시 지연된다. 혹은 잘못된 테스트 코드를 수정할 수도 있다.
  • 테스트코드 이름을 보면 이 테스트가 무엇을 검사하는지 알 수 있는데, 실제로 테스트 메소드명과 애매하게 혹은 관련 없는 테스트하는 경우가 종종있다. 올바르게 검사하는 것도 중요하지만, 의도한대로 테스트하는지도 중요하다. 다른 팀원이 내 테스트 코드를의도한대로 구현했는지 확인이 필요하다.

[2] Testable한 프로덕트 구조

  • 하나의 클래스, 메소드에 많은 기능이 담겨있다면 프로덕트 코드도 지저분하고, 테스트 코드 역시 유지보수하기 힘들어진다.
  • 하나의 메소드에 외부 컴포넌트에 의존하는 코드들과, 로직들이 섞여있다고 해보자. 외부 컴포넌트에 의존하는 것들은 특정 인터페이스, 추상 클래스로 뺴고, 이 부분을 mocking 처리하자. 어짜피 외부 모듈에 대해서도 테스트해야하는거 아닌가?에 대한 의문이 들 수 있는데, 이 부분은 외부 모듈에서 처리하면 된다. 책임도 적절히 나눴고, 이 메서드를 호출할 다른 컴포넌트도 보다 편하게 테스트할 수 있다. 즉, 범용성이 증가한다.
  • 그렇다고 mocking이 무조건 좋은건 아니다. 오히려 mocking 없이 테스트하는게 더 신뢰성이 있을 수 있다. 다만 외부 서비스, db 이런 데이터를 가지고 테스트 코드를 만들기엔 외부 영향이 너무 크게 잡히고, 테스트 중복이 발생할 수 있다. (시나리오가 다르다면 테스트 중복이 아니긴한데, 너무 비슷하면 굳이 안만들어도 되니 외부 모듈에게 테스트를 맡기고 필요하다면 연동시에 추가하자.)

[3] 의존성을 최소화하자.

  • 시간, 랜덤 값, 동시성 이슈, 다른 서비스 의존, 네트워크, 디비 등 외부 의존성을 최소화하여 테스트해야한다. 외부 컴포넌트에 의존하게 되면 외부 컴포넌트 상태에 따라 테스트 코드가 성공하기도, 실패하기도 한다. 우리가 테스트하고자 하는 건 우리 서비스의 특정 기능이지, 외부 컴포넌트는 아니다. 외부 컴포넌트의 테스트는 외부에 맞기자.
  • 외부 서비스와의 의존성을 끊고자 Test Double (mock, stub, spy)을 사용하면, 테스트하고자 하는 서비스 이외의 컴포넌트들간의 의존성을 제거할 수 있다. 이를 사용하면 테스트 대상 코드를 격리하고, 매번 연동할 필요가 없어 실행 속도를 개선하고, 예외 케이스와 같은 특수한 상황을 시뮬레이션할 수 있다.
  • 그렇다고 mocking이 무조건 좋은건 아니다. 오히려 mocking 없이 테스트하는게 더 신뢰성이 있을 수 있다. 다만 외부 서비스, db 이런 데이터를 가지고 테스트 코드를 만들기엔 외부 영향이 너무 크게 잡힌다.

[4] 외부로부터 독립성을 지키자.

  • 다른 테스트 코드, 외부 영향 (db, redis, 외부 서비스 등)에 받는걸 최소화 해야한다. 즉, 외부 모듈에 영향 없이 항상 같은 출력을 내보내야한다.
  • 또한, 테스트 코드로 인해 DB에 영향을 가한다든가, static 값 변경 등 외부 상태를 변경해서는 안된다.
  • 여러 테스트 코드들이 하나의 mocking 데이터를 공유할 수 있는데 조회만 하는 리소스는 괜찮지만 수정하는 데이터는 테스터 전후에 원복하는 과정을 거쳐야한다. 혹은 매번 객체를 할당해야한다.

[5] 신뢰성있는 테스트코드만 남기자.

  • 절대 실패하지 않거나, 항상 실패하는 테스트는 있으나 마나하다. 열번 실행하면 항상 같은 결과가 나와야하며, 그렇지 않으면 빌드할 때마다 개발자는 테스트 성공 여부 때문에 중재해야한다. 이런 경우 과감히 해당 테스트 코드를 지운다.
  • 만약 정상/비정상 테스트 코드가 섞여 있다면, 한번에 모든 테스트 코드를 정상으로 바꾸기엔 현실적으로 불가능하다. '태그'를 통해 확실한 테스트 코드를 거르는 방식도있다. 즉, 태그가 없는 테스트 코드는 이 코드가 정상인지 보고 아니면 정상의 테스트 코드를 만들고, 태그가 붙은 테스트 코드가 실패하면 프로덕트 코드가 문제있는지 확인해야한다.

4. 테스트 코드 사례

[1] 간단한 로직 처리

  • given[테스트에 필요한 데이터, 테스트 할 객체를 생성하고] -> when[검증할 메소드를 호출하여 결과를 가져오고] -> then[그 결과가 맞나에 대한 assert]으로 처리한다.
  • 검증할 땐 호출한 결과가 몇번 호출했는지(verify), 리턴 값은 무엇인지, 어떤 파라미터로 객체의 메서드를 호출했는지(capture) 등을 assert 할 수 있다.
  • ex) 정상 케이스와 edge case 모두를 테스트한다. 이때 mother 데이터(정상 테스트에서 사용할 객체)를 생성 후, 매 테스트마다 이 함수를 호출하여 데이터를 가져온다. 예외 케이스의 경우 mother 데이터를 가져온 후 필요한 부분만 변경하면된다.
  • Runwith(SpringJUnit4ClassRunner.class)를 클래스레벨에 달아, 테스트 속도를 높여주고 테스트 하고 싶은 객체를 생성한다.객체를 생성할 때는 @Autowired, 생성자를 통해 생성, 혹은 mocking하여 만들 수 있다.

[2] 외부 컴포넌트를 mocking

  • Runwith(MockitoJUnitRunner.class)를 클래스 레벨에 달고, 주 테스트 서비스를 @injectMocks, 호출할 외부 컴포넌트를 @Mock 처리 후, 특정 메소드를 stub처리한다.

[3] 같은 프로젝트 내의 소스 단위 integration test

[4] db, redis 등 외부 모듈을 포함한 integration test

[5] data class test

  • domain, config 클래스를 작성해야될까?
    • 테스트 커버리지를 높이기 위한, 테스트 코드는 지양하는게 좋다고 생각한다. 만약 해당 기능 중 테스트하고 싶은게 있다면 예를 들어, @bean으로 가져와서 로직 처리 등이 있을 경우엔 해야되지만 그 외에는 굳이 안넣어도 괜찮은것같다.

[6] 타 팀과의 테스트

5. TDD

Test Driven Development는 매우 짧은 개발 사이클을 반복하는 소프트웨어 개발 프로세스 중 하나이다. 개발자는 먼저 요구사항을 검증하는 자동화된 테스트 케이스를 작성한다. 그런 후에, 그 테스트 케이스를 통과하기 위한 최소한의 코드를 생성한다. 마지막으로 작성한 코드를 표준에 맞도록 리팩토링한다.

[1] TDD Cycle

  1. 작은 테스트를 추가한다.
  2. 모든 테스트를 실행하고, 실패하는 것을 확인한다. 컴파일 되지 않을 수도 있다. 프로덕트 코드를 건드리지 않았으니 당연하다.
  3. 빨리 테스트가 성공하도록 코드를 개발한다. 어떤 죄악(함수가 무조건 특정 상수를 반환)을 저질러도 괜찮다. 빨리 테스트가 성공하도록 만들자.
  4. 모든 테스트를 실행하고, 성공하는 것을 확인한다.
  5. 리팩토링한다.
    • 3번단계에서 죄악을 저질렀다면 이제 모든 케이스에서 성공하도록 여러 테스트 코드를 추가해보자.
    • 테스트는 당연히 실패했고, 이를 성공시키도록 죄악을 치워보자.
    • 기능이 완성되면서 테스트가 성공이된다. 하지만, 코드의 중복이 있다. 코드가 안 이쁠 수 있다. 괜찮다. 테스트코드가 성공하는 내에서 이쁜 코드를 만들어보자. 맘대로 바꿔도된다. 테스트만 성공하면 되니 리팩토링의 두려움도 사라진다.
  6. 개발 중 문뜩 "아 이거해야되는데?" 라는 생각이 든다. 이 때 바로 그 작업을 하지말고 메모장에 'To-Do List'를 추가하자. 이후에 하나하나 To-Do List를 제거하자. 이렇게 하는 이유는 현재 작업하는 부분에 집중하기 위해서다.

[2] TDD는 두려움을 관리한다.

  • 개인적으로 두려움이란 지금 작성중인 로직이 정상적으로 동작하나?에 대한 의구심이라고 생각한다.
  • 복잡한 구현 알고리즘 문제를 풀 때 생각해보자. 처음부터 끝까지 한번에 프로덕트 코드를 만들고, 한번에 테스트를 하는 경우는 드물다. 물론 똑똑한 개발자라면 가능하지만 적어도 본인은 아니다. 큰 기능을 작은 기능으로 쪼개고, 다양한 데이터를 넣어보면서 작은 테스트를 수행한다. 버그가 나오면 어디서 나온지에 대한 포인트를 쉽게 찾기 위해서다. 즉, 단계를 나누고 디버깅을 통해 검증하면서 나아간다.
  • TDD도 유사하다. TDD는 매 순간순간을 자동화된 테스트를[디버깅, curl call에서 테스트로만 바뀐 형태다] 통해서 확인받고 나아가는 개발 방법이다. 이전 단계의 기능을 마음 한켠에 신경쓰지 않고 '현재 단계' 로직만 집중하면된다. 로직이 잘못됬나?에 대한 생각도 현재 로직에만 집중하면된다. 심리적으로 '이전 로직이 틀렸을까?' 요런 불편한 감정을 최소화한다.
  • 만약 복잡한 프로덕트 코드를 한번에 짜고 테스트를 수행하는데 잘못된 점을 발견하면 어떨까? 아마 지금까지 힘들게 짠 프로덕트 코드를 다 수정해야한다. (수정하지 않더라도 정상적으로 동작하는지 머릿속으로 일일이 확인해야한다.) 아깝다. 하지만 TDD를 통해 이전에 점진적으로 검증받았다면, 이전에 개발한 로직은 신경쓰지 않아도된다.
  • 마지막으로 리팩토링할 때 수월하다. 모든 케이스를 검증할 수 없겠지만 기존 테스트 코드가 있으니, 테스트를 만족하기만 한다면 마음 편히 리팩토링할 수 있다. 이건 테스트 코드의 장점이기도 하다.

[3] 현재 하는 일에 집중한다.

  • 복잡한 기능을 한번에 짜기 힘들다. 따라서 작업을 분할하고 이를 합치는 방향으로 개발한다. 앞서 TDD는 각 단계마다 검증하면서 나아간다고 했다. 따라서 분할한 작업에서 오류가 나온다면 이전 단계의 오류는 신경쓰지 않고 현 단게만 생각하면된다. 개발 중 오류를 찾기가 쉽지 않을까?
  • 앞서 To-Do List를 말했다. 작업 중 문뜬 여러 생가이 난다. 이거 고쳐야하고, 이거 추가해야하고 등등이다. 그런 것들은 필요한데에다 메모하고, 원래 하던 작업에 집중할 수 있다. 본인은 막 컴퓨터처럼 스위칭이 자유롭지 않다. 원래 하던일만 잘하고 싶다.

[4] TDD에 대한 다양한 생각

닭이 먼저냐, 달걀이 먼저냐?

  • 테스트 코드를 먼저 만들고, 프로덕트 코드를 만드나 순서를 바꾸는 거에 대한 차이는 크게 없다고 생각한다. 단지 테스트 코드를 먼저 만들면 기능을 사용하는 관점에서 먼저 생각할 수 있다. 요것만 차이가 있는 것 같다. 하지만 이는 TDD를 함으로써 얻기 보단, 객체지향 설계 능력이 좋아지는게 더 우선시 되는 것으로 보인다. ex) public interface

TestCode가 많다면 무조건 좋을까?

  • TDD를 하면 자연스럽게 자동화된 테스트 코드가 많아진다. 테스트 코드가 많은건 edge case를 많이 잡아줄 수 있다는 측면에서 좋다. 그런데 일부분의 코드를 수정하는데 많은 테스트 코드를 수정/추가한다면, 프로덕트 코드보다 테스트 코드를 수정하는데 시간이 많이 걸린다. 결국 우리가 수정한 테스트 코드가 edge case를 잡아주는건 너무 좋지만, 일부 기능을 수정 하는데 테스트 코드 때문에 오래걸리면 시간이 너무 아깝다.
  • 여기서 말하는 시간이 아깝다라는 말은 테스트 자체가 아깝다라는 건 아니다. 내가 고친 로직은 당연히 테스트가 필요하다. 다만 이걸 테스트 코드로 하냐 내가 직접 테스트를 하냐 요 차이가 있는 것 같다.
  • 결국 우리가 만든 로직의 결함을 잡아주기 위해 테스트 코드가 많으면 좋고, 그렇다고 유지보수할 때 테스트 코드에 너무 시간을 쓰기도 아깝다. 다음은 몇가지 테스트 코드 추가에 대한 생각이다.
    1. 절대 edge case가 나오면 안되는 도메인에서는 시간이 걸리더라도 엄격히 테스트 코드를 추가하는게 맞다고 생각한다. ex) 금융, 방산, 보안 등
    2. 핵심 로직이 담긴 부분에 대해서는, 나중에 유지보수시 실수로 로직이 깨지는걸 방지하고자 요건 지켜야돼! 요런 것들이 있으면 부분적으로 테스트 코드를 추가하자.
    3. 실제 서비스 중 예상치 못한 데이터 인입, 시나리오, 잘못된 함수 사용 등이 있다면 요건 나올 때 테스트를 추가하고, 요걸 개발하자.
    4. 맨 처음 프로젝트를 개발할 때와 같이 자주 변경될 것이 예상되는 부분에서는 초기부터 테스트 코드를 추가하진 말고 어느정도 안정화 됬을 때 추가하자. (근데 이 시점을 알기 너무 이상적이다.)
    5. 테스트가 필요한데 수동으로 하기 너무 귀찮다.
    6. 그 외에 관성적으로 테스트를 추가하는건 지양하고싶다. (유지보수가 너무 힘들다.)
  • 논의 필요

[5] 총평

TDD는 작은 과정을 조금씩 쌓아올려나가는 개발 방법론이다. 각 과정마다 테스트를 통해 확신을 얻고, 현재 작업하는 부분에만 신경쓴다. 이를 위해 적절한 분량의 Step을 나누고, TO-DO List를 활용한다.

6. JUnit

  • JUnit이란 자바의 단위테스트를 수행하는 대표적인 Testing Framework로써 다양한 버전이 존재한다.
  • assert 메소드로 테스트 케이스의 수행결과를 판별하며, 다양한 어노테이션이 존재한다. 혹은 verify로 특정 함수 호출 횟수로 검증할 수 있다.

[1] JUnit에서 사용하는 어노테이션

  • @Test
    • 테스트를 만드는 모듈
  • @DisplayName
    • 테스트 클래스 또는 테스트 메서드의 사용자 정의 표시 이름을 정의
  • @BeforeEach / @AfterEach (junit5)
    @Before / @After (junit4)
    • 각 테스트 메서드 전/후에 실행됨을 나타냄
  • @BeforeAll / @AfterAll (junit5)
    @BeforeClass / @AfterClass (junit4)
    • 현재 클래스의 모든 테스트 메서드 전/후에 실행됨을 나타냄

[2] Runwith Annotation

  • @RunWith는 JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 애노테이션이다. (junit5부터는 extendwith)
  • SpringJUnit4ClassRunner라는 JUnit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면 JUnit이 테스트를 진행하는 중에 테스트가 사용할 애플리케이션 컨텍스트를 만들고 관리하는 작업을 진행해준다.
  • 스프링의 JUnit 확장기능은 테스트가 실행되기 전에 딱 한 번만 애플리케이션 컨텍스트를 만들어두고, 테스트 오브젝트가 만들어질 때마다 특별한 방법을 이용해 애플리케이션 컨텍스트 자신을 테스트 오브젝트의 특정 필드에 주입해주는 것이다.
  • @RunWith에 Runner클래스를 설정하면 JUnit에 내장된 Runner대신 그 클래스를 실행한다. 여기서는 스프링 테스트를 위해서 SpringJUnit4ClassRunner라는 Runner 클래스를 설정해 준 것이다.한 클래스내에 여러개의 테스트가 있더라도 어플리케이션 컨텍스트를 초기 한번만 로딩하여 사용하기 때문에, 여러개의 테스트가 있더라도 처음 테스트만 조금 느리고 그 뒤의 테스트들은 빠르다.
  • 이외에도, MockitoJUnitRunner.class는 mock 데이터를 활용하 때 사용한다. PowerMockRunner.class는 powermock 데이터를 활용할 때 사용한다.
  • @ContextConfiguration은 자동으로 만들어줄 애플리케이션 컨텍스트의 설정파일위치를 지정한 것이다. 

[3] Mock

  • 호출에 대한 기대를 명세하고, 해당 내용에 따라 동작하도록 프로그래밍된 객체다.
  • 객체의 메서드가 호출되었을 때 정상 동작은 보장하지 않으며, 객체는 전달되지만 사용되지 않는 객체이다. 호출에 대한 기대를 명세하고, 해당 내용에 따라 동작하도록 프로그래밍된 객체다.
  • 예를 들어 특정 인터페이스의 실 구현체가 필요할 때 구현체를 만들고 실제 기능은 동작하지 않게한다. 이처럼 동작하지 않아도 테스트에는 영향을 미치지 않는 객체를 Mock 객체라고 한다.

사용법

  • class level : @RunWith(MockitoJUnitRunner.class)
  • @Mock : 구현체가 없는 껍데기의 목 객체 생성
  • @InjectMocks : @Mock, @Spy가 붙은 객체를 @InjectMocks 붙은 객체에 주입시킨다.
given(mockRepository.getMemberId(any())).willReturn(new Member("test"));
given(mockRepository.getMemberId(any())).willThrow(new Exception());

참고 (1) PowerMockito Library

  • private 메소드, 함수 내에서 생성한 데이터를 mocking해서 처리 할 수 있다.
  • 테스트하기 굉장히 간단해보이지만 만약, private 메소드가 변경된다면 컴파일 레벨에서 버그를 잡을 수 없는 단점이 있다.
  • 이외에도 reflection으로 처리가 가능하며, junit5부터 mockito 레벨에서 처리할 수 있다.
PowerMockito.doThrow(new IOException()).when(FileUtils.class, "getFile", any(), any());

or

File file = PowerMockito.mock(File.class);
PowerMockito.whenNew(File.class).withAnyArguments().thenReturn(file);
PowerMockito.when(file.exists()).thenReturn(false);

참고 (2) static method mock 처리

  • mockito-inline library
 try (MockedStatic<StaticUtils> utilities = Mockito.mockStatic(StaticUtils.class)) {
        utilities.when(() -> StaticUtils.range(2, 6))
          .thenReturn(Arrays.asList(10, 11, 12));

        assertThat(StaticUtils.range(2, 6)).containsExactly(10, 11, 12);
    }

논의하고 싶은 내용

  1. 테스트 코드가 항상 필요한가?

  2. TDD 빙식으로 개발하는게 좋나? 작개쪼개기, To Do List만 가져오는게 괜찮지 않을까?

  3. mocking하는 테스트 코드

profile
성장하는 개발자가 되고싶어요

0개의 댓글