ALM/Test Automation

단위테스트 3회 - 커버러지 분석과 단위 성능 테스트

Terry Cho 2008. 3. 12. 09:36

테스트 코드 커버러지와 단위 부하 테스트

(Test Code Coverage & Unit performance test)

 

자바스터디 조대협 (http://bcho.tistory.com)

현재 BEA Systems Korea에서 Senior 컨설턴트로 엔터프라이즈 애플리케이션 개발과 미들웨어 SOA에 대해 컨설팅을 진행하고 있다.

온라인 자바 사이트 http://www.javastudy.co.kr 의 초기 시샵이며, 한국 자바 개발자 협의회 JCO의 초대 부회장을 맏았다.

 

이번 글에서는 테스트가 애플리케이션을 어느정도 테스트했는지를 측정하는 코드 커버러지와, Japex 테스트 프레임웍을 이용한 부하 테스트 방법에 대해서 알아보도록 한다.

1.코드 커버러지 (Code Coverage)

* 테스트 커버러지란?
우리가 단위 테스트나 통합 테스트와 같은 일련의 테스트 작업을 수행하였을때, 이 테스트가 전체 테스트를 해야 하는 부분중에서 얼마만큼을 테스트 했는지를 판단해야 한다.
예를 들어, 20가지의 기능을 가지고 있는 애플리케이션이 있을때, 몇가지 기능에 대해서 테스트를 했는가와 같이, 수행한 테스트가 테스트의 대상을 얼마나 커버했는지를 나타내는 것이 테스트 커버러지이다. 이 커버러지율을 기준으로 애플리케이션이 릴리즈가 가능한 수준으로 검증이 되었는가를 판단하게 된다.

위에서 예를 든것과 같이 기능에 대한 테스트 완료여부를 커버러지의 척도로 삼을 수 도 있다.
좀더 작은 범위의 테스트인 단위 테스트의 경우는 개개의 클래스나 논리적인 단위의 컴포넌트 각각을 테스트하기 때문에, 테스트에 대한 커버 범위를 각각의 클래스 또는 소스 코드의 각 라인을 척도로 삼을 수 있는데, 테스트가 전체 소스코드중에서 얼마나를 커버했는지를 나타내는것이 "코드 커버러지(Code Coverage)" 이다.

* 코드 커버러지 툴의 원리
코드 커버러지 툴의 주요 기능은 실행중에 해당 코드라인이 수행이 되었는가? 아닌가를 검증하는것이다. 이를 위해서 커버러지 툴은 클래스의 각 실행 라인에 커버러지 툴로 로깅을 하는 로직을 추가 하는 것이 기본 원리이다.

예를 들어 아래와 같이 간단한 HelloWorld라는 소스가 있을 때, 소스 커버러지 툴을 거치게 되면 <그림 2> 와 같은 형태의 소스 코드를 생성해내게 된다.

public class HelloWorld(){
  public void HelloBcho(){

  System.out.println(Hello Bcho);

}

public void HelloHyunju(){

System.out.println(Hello Hyunju);

}

}

< 그림 1. 원본 소스 코드 >

 

public class HelloWorld(){
  public void HelloBcho(){

    CoverageTools.log(클래스 및 라인 관련 정보1);

  System.out.println(Hello Bcho);

CoverageTools.log(클래스 및 라인 관련 정보2);

}

public void HelloHyunju(){

CoverageTools.log(클래스 및 라인 관련 정보3);

System.out.println(Hello Hyunju);

CoverageTools.log(클래스 및 라인 관련 정보4);

}

}

< 그림 2. 코드 커버러지 로그 수집이 추가된 코드>

 

그림 2와 같이 변형된 코드가 수행되게 되면 코드 수행에 따른 로그가 생성되게 되고, 이를 분석하여 코드중에 어느 부분이 수행되였는지를 보여주는 것이 코드 커버러지 툴의 기능이다.

이렇게 기존의 클래스에 커버러지 분석을 위한 분석 코드를 추가 하는 작업을 instrument 라고 하고 크게 정적 기법과 동적 기법 두가지를 지원한다.

정적 기법은 애플리케이션이 수행되기 이전에 소스코드나 컴파일이 완료되어 있는 클래스 파일을 instrument하여 instrumented class들을 만든후 그것을 수행하는 방식이고, 원본 class를 가지고 애플리케이션을 수행하여 런타임시에 클래스가 로딩되는 순간에 클래스에 Instrumentation을 하는 것이 동적 방식이다.

 동적 방식은 AOP (Aspect Oriented Programming)이나, APM (Application Performance Monitoring)에서 많이 사용하는 방법이다. 그러나 이 방식의 경우 런타임에서 code instrumentation을 하는 부하가 발생하기 때문에, instrumentation양이 AOP APM에 비해서 압도적으로 많은 Code Coverage툴의 경우에는 정적인 instrumentation 방식이 좀더 유리 하다. 단 정적 방식의 경우 컴파일후에도 instrumentation을 한번 거쳐야 하고, coverage를 분석하기 위한 애플리케이션 묶음(JAR,WAR)과 운영을 위한 애플리케이션 묶음이 다르기 때문에 용도에 따라서 매번 다시 배포(DEPLOY)해야 하는 번거로움이 있다.

 툴에 따라서 instrumentation 방식이 다르기 때문에 애플리케이션의 성격과 규모에 따라서 적절한 instrumentation을 사용하는 툴을 사용하기 바란다.

 

* Cobertura를 이용한 커버러지 분석

많이 사용하는 코드 커버러지 분석 도구로는 상용으로 www.atlassian.com Clover, http://www.instantiations.com/ Code Pro Analytix 등이 있다.

대표적인 오픈소스로는 EMMA Cobertura가 있다. EMMA의 경우 동적,정적 Instrumentation을 모두 지원하며, Eclipse 플러그인도 지원한다.

Cobertura의 경우 정적 Instrumentation만 지원하지만 사용 방법이 매우 쉽기 때문에, 여기서는 Cobertura를 이용한 Code Coverage분석 방법을 설명한다.

Cobertura를 이용하기 위해서는 http://cobertura.sourceforge.net 에서 다운 받아서 설치한다.

<!-- 1)클래스 패스 정의 -->

<path id="cobertura.class.path">

  <pathelement location="${cobertura.home}/cobertura.jar"/>

  <fileset dir="${cobertura.home}/lib" includes="*.jar" />

</path>

 

<!-- 2) 태스크 정의 -->

<taskdef resource="tasks.properties" classpathref="cobertura.class.path" />

 

<!-- 3) 빌드 -->

<target name="build">

<!-- 컴파일후에, EAR 파일로 패키징을 한다. -->

<javac debug=true >

</target>

 

<!-- 4) Code Instrumentation -->

<target name="instrument_cobertura" dependes="build">

 <cobertura-instrument todir="${workspace.dir}"

  datafile="${workspace.dir}/cobertura.ser" >

   <includeClasses regex="bcho.*" />

   <instrumentationClasspath>

      <pathelement location="${ear.file}/>

   </instrumentationClasspath>

  </cobertura-instrument>

</target>

 

<!-- 5)Code Coverage Report generation -->

<target name="report_cobertura" >

   <cobertura-report format="html" desdir="리포트 HTML이 저장될 디렉토리"

      datafile="${workspace.dir}/cobertura.ser">

      <fileset dir="원본 소스 코드가 있는 디렉토리 >

           <include name="**/*.java" />

   </cobertura-report>

</target>

< 그림 3 . Cobertura 사용을 위한 ANT TASK >

 

1) Cobertura를 인스톨한 디렉리를 ${cobertura.home} Property로 정의하고, 클래스 패스를 지정한다.

2) Cobertura에 대한 ANT TASK를 이용하기 위해서 <taskdef>를 이용하여 cobertura classpath에 정의된 TASK를 정의한다.

3) 다음으로 애플리케이션을 빌드하고, JAR WAR,EAR등으로 패키징한다. (Instrument code를 넣기 위해서 패키징은 필수 사항은 아니다. ) 이때 커버러지 분석에서 결과가 제대로 표시되게 하기 위해서 <javac> 컴파일 옵션에 debug=true를 추가한다.

4) 컴파일과 패키징이 완료된 자바 애플리케이션에 대한 Instrumentation을 수행하기 위해서 <cobertura-instrument> 테스크를 사용한다.

 todirInstrument된 애플리케이션 (EAR Instrument할 경우, Instrument EAR파일이 이 경로에 저장된다.) 저장될 디렉토리이며, datafile은 커버러지를 분석한 결과 데이터를 저장할 파일명이다.

 다음으로 커버러지를 분석할 클래스명을 지정하는데, <includeClasses> 라는 엘리먼트를 이용하여, regular expression을 사용하여 클래스의 범위를 지정한다. 예제에서는 bcho.*로 시작하는 패키지의 클래스만 Instrument를 하도록 지정하였다.

 그리고 마지막에 Instrument를 할 class 파일들의 위치를 지정하는데, <instrumentationClasssPath> class들이 저장된 경로를 지정하거나 또는 EAR등으로 패키징된 파일 경로를 지정한다.

 예제에서는 EAR파일을 Instrument하는것으로 지정하였다.

여기까지 진행한후 ant instrument_cobertura를 수행하면, ${workspace.dir} instrumented ear 파일이 생성된다.

 

Instrumented 된 애플리케이션을 서버에 배포하거나 단독 수행 애플리케이션일 경우 실행을 한다. 이때 JUnit을 이용한 테스트를 수행해도 되고, 손으로 직접 기능을 수행해봐도 된다. Instrumented 된 애플리케이션은 모든 수행 결과에 대한 로그를 저장하기 때문에 특정 테스팅 프레임웍을 사용하지 않아도 된다.

이때 수행되는 애플리케이션에 Cobertura에 대한 Coverage 로그를 저장할 디렉토리를 지정해야 하는데, JAVA에서 -D옵션을 이용하여

 -Dnet.sourceforge.cobertura.datafile=위의 ANT 스크립트에서 지정한 *.ser 파일의 절대 경로

를 지정해야 *.ser파일에 제대로 커버러지 분석 결과가 저장된다.

 

필자의 경우에는 JUnit + Cactus를 이용하여 EJB 2.1 WebLogic에서 수행하였다.

애플리케이션의 실행 또는 테스트가 끝난후 애플리케이션을 종료하면 (WAS의 경우 WAS를 종료해야 한다.) 종료시에 커버러지 데이터를 *.ser 파일에 기록하게 된다. (런타임시에는 *.ser파일에 결과가 기록되지 않는다.)

 

*.ser파일에 기록된 커버러지 분석 값은 바이너리 형태로 사용자가 볼 수 없기 때문에 이를 리포트로 생성한다.

<그림 3>에서 "report_cobertura 라는 타겟을 정의하였는데,

<cobertura-report> 엘리먼트에서 HTML 리포트가 저장될 디렉토리를 destdir로 지정하고, *.ser의 위치를 datafile attribute로 지정한다.

 그리고, 원본 소스 코드가 있는 디렉토리를 <fileset>으로 지정해주면 destdir HTML형식으로 커버러지 분석 리포트가 생성된다.

사용자 삽입 이미지

<그림 4. 테스트 커버러지 클래스 레벨 리포트 >

그림 4에서 보는것과 같이 전체 코드에 대해서 테스트가 전체 코드중 몇 %를 수행했는지를 나타내 준다. 결과에서 주의할점은 HelloEJBBean의 경우 86%의 커버러지가 나오지만 나머지 클래스들은 N/A 또는 0%가 나온다.

 이는 EJB 2.1로 코딩한 EJB의 경우 사용자는 HelloEJBBean만 코딩을 하지만 나머지 클래스들은 빌드과정에서 WAS가 자동으로 생성해주는데, 자동으로 생성된 클래스(Home, Interface,Stub )에 대한 위치 정보가 없기 때문에, N/A또는 0으로 그 결과가 표시되는 것이다. 이런 경우에는 <cobertura-instrument> 엘리먼트에서 <includeClasses>와 같은 엘리먼트로 <excludedClasses>라는 엘리먼트를 이용하여 특정 클래스들을 Instrumentation에서 제거할 수 있다.

 이런 경우에는 <excludedClasses regex=*Home /> 등을 정의해서 자동 생성되는 클래스들을 제거하거나 또는 반대로, <included Classes=bcho.*\.*Bean/> 으로 해서 bcho.* 패키지에서 Bean으로 지정되는 부분에 대해서만 Instrumentation을 수행할 수 있다.

 

사용자 삽입 이미지

<그림 5. 테스트 커버러리 코드 레벨 리포트 >

 

연두색으로 표시되는 부분, (27,35,41,42,43,47 라인)은 수행된 라인이고 그 옆에 숫자는 수행된 횟수를 나타낸다.

분석 결과를 보면 sayHello 메서드에서 isMale이 항상 true 14번 수행이 되었고 false인 경우에 대한 코드는 수행 되지 않았음을 확인할 수 있다.

 

여기 까지 간단하게 Cobertura를 이용한 코드 커버러지 분석 방법에 대해서 알아보았다.

 

* 코드 커버러지의 척도

코드 커버러지 분석에서 가장 어렵고 논란의 여지가 많은 부분은 목표 커버러지율이다. 혹자들은 테스트가 80% 이상의 코드 커버러지를 가져야 한다고 주장하는데, 일반적인 Value Object 그 많은 Util 클래스등등을 감안할 때, 80%는 상당히 높은 수준이다. 필자의 경우에는 60%도 쉽지 않는 수준이라도 보는데, 이 척도에 대한 기술적인 지표가 필요할 경우 CMM(Capability Maturity Model)등을 참고하기 바란다.

 개인적인 생각으로는 Coverage rate를 올리는 것 보다 중요도가 높은 모듈의 Coverage rate만을 관리하는 것이 효율적이라고 생각한다.

 전체 클래스가 1000개가 있을 때, 이 클래스들을 논리적인 컴포넌트로 묶은 후 (UML Conceptual Mode, Logical Model, Implementation Model Logical Model 단위로) 이 소프트웨어 컴포넌트에 대한 위험도와 복잡도를 지정하여 이를 Ranking하여 상위 순서에서 기준에 따라서 중요 컴포넌트만 Test Coverage를 관리하도록 하는 것을 권장한다.

 커버러지 분석에 대한 수치는 고객이나 메니져 입장에서 전체 시스템중 80%가 테스트 되었습니다.와 같은 의미로 받아 들여지기 때문에, 30%,60% 와 같은 기준은 받아들여지기가 어렵다. 몇 달은 테스트를 했는데, 60% rate로 매우 중요한 시스템을 오픈한다면 당신이라면 accept하겠는가?

 그렇지만 실제 애플리케이션의 전체 코드를 테스트 하는 것은 어렵고 대부분 중요 문제는 상위 10~30%의 모듈에서 발생한다. 만약 100%의 테스트 커버러지가 가능하다면 IBM이나 MS와 같은 첨단 거대 기업에서 만든 소프트웨어들에서 버그가 존재하고, 우주로 쏘아 올리는 로켓에서 문제가 생기겠는가?

 그래서 테스트 코드 커버러지를 도입하기 전에 무엇보다 중요한 것은 어떤 툴이나 커버러지 분석을 자동화하는 것이 아니라, 코드 커버러지를 적용할 소프트웨어 컴포넌트의 범위와 목표 커버러지율을 정의하고 이에 대해서 이해 당사자 (Stakeholder-고객,메니져 등)로부터 동의를 얻어내는 것이 선행되어야 한다.

 그후에 테스트 코드 커버러지 검사를 통해서 특히 단위 테스트의 정확도를 높이는 방향으로 애플리케이션의 완성도를 높이는 방향으로 개발을 진행하도록 한다.

 

2.부하 테스트 (Stress Test)

마지막으로 살펴볼 부분은 단위테스트에 대한 부하테스트에 대해서 살펴보고자 한다.

 

* 부하 테스트

부하테스트는 대상 애플리케이션에 동시에 대용량의 요청을 넣어서, 애플리케이션의 가용 용량, 속도와 같은 성능적인 척도를 측정하고, 대용량 요청시에도 애플리케이션이 문제가 없이 작동하는지를 확인하는 과정이다. (1회의 테스트 과정에서 설명하였다.)

보통 Load Runner Apache JMeter,MS WebStress등을 이용하여 애플리케이션의 기능에 대해서 부하 테스트를 수행하지만, 여기서 설명하고자 하는 부하테스트는 소프트웨어 컴포넌트에 대한 부하 테스트 이다.  필자의 경우 이를 국지적 부하테스트 라고 정의 하는데, 단위 테스트에 대해서 부하를 넣어서 테스트를 진행하는 방법이다.

 부하 상태에서 소프트웨어 컴포넌트에서 발생할 수 있는 문제는 크게 응답 시간 저하와, synchronized 또는 IO에 의한 lock 경합 현상,deadlock 그리고 메모리 사용량에 대한 이슈가 가장 크다. 이런 문제는 대규모의 전체 애플리케이션 부하테스트가 아니더라도 국지적인 단위 테스트에 대해서 부하를 줄 경우에도 어느정도 검출이 가능하다.

 

이런 단위테스트에 대한 부하 테스트로는 성능 테스트 프레임웍으로 JUnitPerf등이 있지만 여기서는 Japex라는 테스팅 프레임웍을 설명하도록 한다. (http://japex.dev.java.net )

Japex의 경우 별도의 코딩을 할 필요가 없이 기존의 JUnit에 설정을 추가하는 것만으로 JUnit에 대해서 부하 테스트가 가능하다.

 

* Japex를 이용한 부하 테스트

Japex를 사용하기 위해서는 http://japex.dev.java.net에서 Japex 라이브러리를 다운 받은후

java com.sun.japex.Japex config파일

로 수행하면 된다.

또는 같은 방식으로 ANT task에서 정의할 수 있다.

<pathelement id="class.path">

  <pathelement location="컴파일된 애플리케이션 클래스패스"/>

  <fileset dir="${JAPEX_HOME}/lib" includes="*.jar"/>

  <fileset dir="${JAPEX_HOME}/jsdl" includes="*.jar"/>

  <fileset dir="컴파일된 애플리케이션 클래스패스"/>

</pathelement>

 

<target name="run_japex">

 <java dir=." fork="true" classname="com.sun.japex.Japex" >

   <classpath refid="class.path" />

   <arg line="${Japex config 파일}" />

 </java>

</target>

< 그림 6. Japex ANT 스크립트 >

 

다음으로 japex config 파일을 지정해야 하는데 다음과 같이 지정한다.

<?xml version="1.0" encoding="UTF-8"?>

<testSuite name="ParsingPerf" xmlns="http://www.sun.com/japex/testSuite" >

 <param name="japex.classPath" value="애플리케이션이 빌드된 디렉토리 (클래스 경로)" />

 <param name="japex.classPath" value="Japex home/jsdl/*.jar" />

 <param name="japex.resultUnit" value="ms" />

 <param name="japex.wrampupTime" value="0" />

 <param name="japex.runtime" value="00:03" />

 <param name="japex.numberOfThreads" value="20" />

 

 <driver name="JUnitDriver">

   <param name="japex.driverClass" value="com.sun.japex.jdsl.junit.JUnitDriver"/>

 </driver>

 

 <testCase name="testHello">

  <param name="testName" value="bcho.test.junit.HelloWorldTestCaseWrapper" />

 </testCase>

</testSuite>

 

<그림 7. Japex config 파일 ${Japex config 파일} >

 

중요한 parameter로는

l        japex.resultUnit 은 성능 결과치에 대한 단위 (, 밀리세컨드 등)

l        japex.wrampumTime은 성능 테스트시 본 테스트전에 시스템을 워밍업 하는 시간 (캐쉬 초기화등등).

l        japex.runtime은 실제 테스트를 수행하는 시간

l        japex.numberOfThreads는 동시에 부하를 주는 클라이언트 수이다.

그리고 JUnit Japex로 호출하기 위해서 driver JUnit 드라이버를 지정하여 로딩하고 <testCase> 엘리먼트에 testName으로 JUnit 테스트 슈트를 지정하여 해당 슈트가 Japex의 설정에 따라 부하테스트가 되도록 한다.

테스트가 끝나면 테스트 결과를 다음과 같이 여러 형태의 그래프로 표현할 수 있다.

사용자 삽입 이미지 사용자 삽입 이미지

< 그림 8. Japex로 생성한 성능 테스트 그래프 >

 

매우 간단하게 Japex를 이용한 단위 부하테스트를 소개했지만, Japex JUnit 뿐만 아니라 driver를 추가함으로써 여러 형태의 부하테스트를 지원할 수 있고, 상당히 여러 종류의 그래프를 출력할 수 있으며 개발자에게 별도의 개발이 없이 기존의 JUnit을 재활용하여 부하테스트를 가능하게 해주기 때문에, 상당히 유용한 테스팅 프레임웍이다.

 

3. 맺음말

지금까지 3회에 걸쳐서 자바 단위 테스트의 기본이 되는 JUnit, J2EE에서 유용한 DBUnit Cactus와 같은 확장된 프레임웍, 테스트 검증을 위한 커버러지 분석툴 그리고 단위 부하테스트까지 간단하게 살펴보았다.

사실 테스트에 대한 범위는 매우 넓고 광범위 하다. 우리가 살펴본 내용은 이제 막 걸음마를 뗀 수준에 불과 하지만, 단위 테스트가 무엇이고 어떻게 진행해야 할것인가에 대한 방향성을 제시하기 위한 내용이다.

 반드시 기억해야 하는 내용은

 테스트는 도입해야 한다.

 어떤 테스트 툴을 도입 하는가 보다는 어떻게 테스트를 할것인가?

가 가장 중요하다.

 

그리드형