[Junit] 단위 테스트를 작성하는 이유

단위 테스트를 작성하는 이유

두 개발자가 작업하는 모습을 살펴보면 단위 테스트가 필요한 이유를 어렴풋이 알 수 있다. 이 예시는 자바와 JUnit을 활용한 실용주의 단위테스트에서 보여주는 예시에 내가 본 얘기를 조금 첨가했다.

개발자 A의 경우

개발자A에게 VOC(Voice of Customer), VOU(Voice of User), SR(System Request) 라고 불리는 이른바 수정사항이 주어졌다고 하자.

이렇게 하면 되겠네 라고 생각하며 개발자A는 코드 20여 줄을 시스템에 추가하는 작은 작업을 마쳤다. 그리고 빌드 스크립트를 실행하고, 커피 한 잔 하러 나갈 생각하며 변경된 코드를 패키징해 로컬 웹 서버에 배포한다.

그런데, 웹 어플리케이션을 실행하고 데이터를 입력하고 제출하니 스택 트레이스가 발생한다. 엥 뭐야, 라고 생각하면서 에러를 읽다가 필드 초기화를 빼먹은 것을 잊은 사실을 알아채고 오류를 수정한다.

아니나 다를까 이번에는 다른 값이 나타난다. 조금 더 오랜 시간을 소요해 디버거를 이용해 한 끗 차이 오류 (off-by-one error)를 찾아낸다. 겨우 오류를 수정한 뒤 테스트를 진행해 정상적으로 동작하는 것을 확인한다.

여기까지 아주 짧은 수정을 거쳐 동작이 되는 것을 확인하는데 20분이 소요됐다. 이런 자잘한 시간들이 모이고 모여 결국, 개발자A의 팀은 소프트웨어 납기일을 한 달 연장하기 위한 PPT나 만들고 있는 자신들을 보게된다.

개발자B의 경우

개발자B는 코드를 작성할 때마다 시스템에 추가되는 작은 변화를 검증할 수 있는 단위 테스트도 작성한다. SRE 팀에서 지표 관리 대상으로 지정되지도 않았는데, 그런 일을 왜 하냐는 핀잔도 듣는다. 뭐 어쨌든, 개발자B는 모든 단위 테스트를 작성한다.

처음엔 몰랐는데, 개발이 점점 진행되면서 이 테스트 코드들이 빛을 보기 시작한다. 기능 테스트는 몇 분 안으로 모두 완료되고, 어디가 잘 못 됐는지 정확한 부분을 가르쳐주어 하루 종일 로그를 쳐다보지 않아도 되었다.

운영팀으로의 인수인계도 이전과 달리 부드럽게 진행된다. 테스트 시나리오로 트집잡고, 파워 유저 선정으로 싸우던 시간이 데이터 검증과 코드 리뷰로 대체된다.

다른 사람이 같은 영역의 코드를 변경할 때도 테스트는 동일하게 동작한다. 그렇기 때문에 어떻게 동작해야 하는지 운영 담당자에게 가이드 라인을 제시하는 것과 다를 바가 없다.

개발자B가 간만에 퇴근하려고 짐을 싸고 일어서니, 반대편 동기 개발자A 모니터에 빈 파워포인트 화면이 보인다.

서론

개발자B의 이야기는… 사실 소설같은 얘기다. 테스트 커버리지 100%를 달성한다고 모든 IT 조직이 가진 문제가 마법처럼 해결되진 않을 것이다. 그치만 10개 터질 문제를 3개로만 줄여도, 대단한 발전이 아닐까? 고민할 시간에 지금 당장 시작해보자.

단위 테스트란?

Unit Test 란 컴퓨터 프로그래밍에서 소스 코드의 특정 모듈이 의도된 대로 정확히 작동하는지 검증하는 절차를 뜻한다. 달리 표현하면 이는 모든 함수와 메소드에 대한 테스트 케이스를 작성하는 절차라 할 수 있다.

이러한 테스트 코드는 다음과 같은 측면에서 중요성을 지닌다.

  • 개발 과정에서 문제를 조기에 발견할 수 있음
  • 코드 변경 시, 변경 부분으로 인한 영향도를 쉽게 파악할 수 있음
  • 코드 리팩토링 과정을 안정적으로 지원함
  • 테스트 코드를 통해 동작하는 방식을 확인할 수 있음

좋은 테스트란? : FIRST 속성

Junit 으로 생산성을 높이기 위해서는 테스트 대상과 커버하는 경계 조건, 좋은 테스트 요건을 알아야 한다. 다행히 많은 테스터들의 생태계에서 공통적으로 받아들여지는 좋은 테스트의 요건 5가지가 있다. 공교롭게도 이 공통 조건들의 영문자 앞 글자들을 잘 조합하면 기억하기 쉽게 FIRST 라는 단어가 나온다. 정리하자면 Fast(빠르고), Isolated(고립되고), Repeatable(반복 가능하고), Self-validating(스스로 검증 가능하고), Timely(적시, 즉시) 작성된 테스트가 좋은 테스트라고 칭해진다.

Fast (빠른) 테스트가 좋은 테스트

  • 한 번 테스트를 하는데 걸리는 시간이 줄 수록 좋은 테스트 코드이다
  • 느린 영속성 저장소 → Database, Local HDD
  • 빠르게 바꾸는 방법
    • 한 번에 조회해서 메모리에 올림
    • 느린 것에 의존하는 코드를 최소화 시킴
    • 미리 테스트 객체를 만들어 놓음
    • 이러한 방법은 추후에 배울 예정

Isolated (고립된) 테스트가 좋은 테스트

  • 작은 양의 코드에 집중한다는 뜻
  • 직접적 혹은 간접적으로 테스트 하려는 메서드와 상호 작용하는 코드가 많을 수록 문제가 발생할 소지가 늘어남
    • 데이터베이스에서 데이터를 가져와 테스트를 진행하는 경우
  • 고립된 테스트 코드는 시간, 순서에 상관 없이 실행될 수 있는 특징을 가짐
  • 객체 지향 클래스 설계의 단일 책임 원칙과 연관지어 자연스럽게 고려될 수 있음

Repeatable (반복가능한) 테스트가 좋은 테스트

  • 실행할 때마다 결과가 같아야 함을 뜻 함
  • 계속해서 정보가 바뀌는 경우에도 인위적으로 주입된 상태를 유지해 일관성 유지하는 방향을 잡자

Self-validating (스스로 검증 가능한) 테스트가 좋은 테스트

  • 시스템의 자가 검증성 과 연관지어 생각해 볼 수 있음
  • 어떠한 부분에 결함이 발생했을 때, 곧바로 이를 발견할 수 있는 특징
  • 테스트에서 발생하는 에러가 해결 방향을 지시하고 제안할 수 있는 특징을 뜻 함
  • 발생하는 모든 변화에 대응하는 것을 최종 목표로 생각해 볼 수 있다 (CI/CD)

Timely (즉시 작성하는) 테스트가 좋은 테스트

  • 테스트를 진행해야 하는 그 순간에 바로 만들어지는 테스트
  • 적시 그리고 즉시 테스트를 작성해야 함

Junit 으로 TC(Test-Case) 작성하기

단위 테스트(Unit Test)란 무엇인가와 어떤 테스트가 좋은 테스트인가를 짤막하게 다뤘다. 단위 테스트를 짤막하게 다시 한 번 정리하자면, 테스트 대상(SUT, System Under Test)을 논리적으로 독립된 작은 코드 조각인 함수나 메소드 단위로 쪼개서 테스트하는 것을 뜻한다. 즉, 우리는 클래스 내의 모든 메소드들이 올바르게 동작하는지를 테스트하는 것을 목적으로 한다.

그럼 단위 테스트는 어떤 이로움을 줄 수 있는가. 아래 그림을 보면 개발 단계의 뒤로 갈수록 결함 발생 빈도는 적지만, 결함 수정 비용은 엄청나게 늘어나는 걸 볼 수 있다. 따라서 소프트웨어 개발 초기 단계에서 Unit Test 를 수행하면 추후 발생할 높은 Cost 의 에러를 줄일 수 있다.

통합 및 시스템 테스트만 수행한다면 문제 발생이 Software 통합 시점에 나타나며 문제 원인 분석 및 수정이 쉽지 않다. 반면 Unit Test 를 잘하면 개발 단계에서 즉각적인 문제 원인 분석 및 수정이 가능한 것이다.

예방의 관점을 넘어 유지ㆍ보수 관점에서도 단위 테스트는 좋은 영향을 준다. 실제로 프로젝트를 수행할 때, 각 사업부별 특화 기능에 필요한 공통 모듈들을 변경할 일이 생겼다. 해당 소스의 변경이 어떤 영향을 끼칠지 몰랐고, 터지는 에러들이 어디서 발생하는지 찾느라 오랜 시간이 걸렸던 기억이 난다. 만약 Test Code 가 완벽히 작성되어있었다면? 조금은 기능 개선이 더 빠르지 않았을까 생각한다.

테스트를 잘 하려면?

의존 컴포넌트(DOC, Depended On Component) 와의 의존성을 끊는 다양한 방법이나 좋은 테스트 속성들을 준수하는 것도 중요하다. 그치만 좋은 테스트를 작성하기 위해선 잘 설계된 소프트웨어와 테스트를 작성하는 사람의 경험이 무엇보다 큰 영향을 가진다.

Test Coverage 100% 은 소프트웨어 품질을 높여주는 마법이 아님을 명심하고, 더욱 촘촘하고 유의미한 테스트를 작성하는데 노력하는 것. 그런 태도들이 모이면 곧 좋은 테스트가 작성될 것이다.

Junit 시작하기

Junit 을 시작하면 아래 과정을 거치게 된다.

  • 입력값에 대한 결과값 검증 : 테스트 대상 메소드에 특정 input 넣었을 때 예상되는 결과와 실행했을 때 return 값 동일한지 확인
  • Exception 발생 검증 : 테스트 대상 메소드 내 Exception 발생시키는 코드가 있을 경우, 조건에 따른 Exception 발생 여부 검증
  • Performance 검증 : 테스트 대상 메소드의 Performance 검증이 필요하다면 time-out 발생 여부 검증

기본 내용 정리

  • Annotations
    • @BeforeClass: 테스트 클래스가 시작되는 시점에 호출되는 Class Setup Method
    • @AfterClass: 테스트 클래스가 종료되는 시점에 호출되는 Class Teardown Method
    • @Before: 각 Test Case 실행 전에 호출되어 초기 환경, 리소스 등을 초기화하는 Setup Method
    • @After: 각 Test Case 종료 이후 호출되어 Setup 시 설정한 환경, 리소스 등을 해제하는 Teardown Method
    • @Test: 테스트 대상 Method 로 한 개의 메소드를 Test Case 라고 지칭
    • @Test(timeout=ms): Test Case 에 ms 단위 실행 조건을 통해 효율성, 로직 검증
    • @Ignore: 해당 Test Case 실행 skip
    • @Rule: 클래스 내 모든 Test Case 실행 전후에 미리 정의된, 혹은 사용자가 작성한 추가 코드를 실행
  • Rules
    • TemporaryFolder: 테스트 전후로 임시 폴더 및 파일을 생성/삭제
    • ExternalResource: 외부 리소스에 대한 전후처리
    • ExpectedException: 테스트 클래스 전체에서 발생하는 예외 확인
    • ErrorCollector: Test Case 실패 시에도 멈추지 않고 테스트를 진행하며, 발생한 오류를 수집
    • Verifier: 테스트 실행 시 추가 검증을 하도록 도와줌
    • TestName: 실행하는 테스트 메소드 이름 표시
    • RuleChain: 여러 Rule 을 연결되도록 묶어 적용
    • ClassRule: 테스트 클래스 전체에 적용
    • TimeOut: 테스트 클래스 전체 Test Case 에 Time out 설정
  • Major Assersions
    • assertEquals: 예상한 값과 실제 값이 같은지 검증하며, float 형 비교시 parameter 로 정밀도를 필요로 함
    • assertArrayEquals: 예상한 배열과 실제 배열이 같은지 검증
    • assertTrue: 특정 조건이 참인지 검증
    • assertFalse: 특정 조건이 거짓인지 검증
    • assertNull: 객체가 null 인지 검증
    • assertNotNull: 객체가 null 이 아닌지 검증
    • assertSame: 두 변수가 같은 객체를 참조하는지 검증
    • assertNotSame: 두 변수가 같은 객체를 참조하지 않는지 검증
    • assertThat: Matcher 를 통한 조건 부합 여부 검증
    • fail: Exception 발생 검증을 위해 고의적으로 AssertionFailedError 발생

Updated:

Leave a comment