대메뉴 바로가기 본문 바로가기

데이터 기술 자료

데이터 기술 자료 상세보기
제목 타깃 기반의 소프트웨어 동적 커버리지 및 성능 측정
등록일 조회수 8838
첨부파일  

소프트웨어 테스트 길라잡이

타깃 기반의 소프트웨어 동적 커버리지 및 성능 측정



바야흐로 IT융합 시대다. 소프트웨어 시장이 확대됨에 따라 소프트웨어의 중요성이 대두하고 복잡도가 증가하면서 소프트웨어 품질 또한 강조되고 있다. 이와 함께 소프트웨어 품질을 측정하기 위한 소프트웨어 테스트도 주목 받고 있다. 이 글에서는 소프트웨어 테스트에 대한 이모저모를 알아본다.



우리 주변에서는 소프트웨어 결함으로 인해 발생하는 인명 사고, 물질적 재산 사고, 브랜드 가치 추락 등의 사태를 흔히 발견할 수 있다. 대표적으로는 나로호 발사 연기 사건이 있다. 2009년 8월 19일 오후 5시 대한민국 고흥군의 나로우주센터에서 나로호의 첫 발사가 시도됐으나 4시 52분 4초, 발사 7분 56초를 남기고 고압 탱크 압력측정 소프트웨어 결함이 발생해 자동으로 발사 중지 명령이 내려졌다.

<그림 2>는 ETCS의 소프트웨어 결함이 자동차 급발진을 일으켜 일어난 사건이다. 오류가 있을 때 이를 보완해주는 방어수단(fail safes)조차 작동하지 않아 사고가 났고 사망자가 발생했다. 결국, 자동차 회사는 제품을 리콜했고 브랜드 가치가 하락했다.





암스테르담 시는 소프트웨어 결함으로 1억8000만 유로에 해당하는 금전적 손해를 봤다. 암스테르담 시에서 책정한 보조금의 액수는 총 180만 유로였다. 그러나 소프트웨어 오류로 1억8000만 유로를 지출하게 됐다. 시 당국은 잘못 지급된 보조금을 환수하기 위해 노력했지만, 지출 금액의 1.3%인 240만 유로밖에 복구하지 못했다. 더불어 암스테르담 시는 소프트웨어를 수정하기 위해 추가로 30만 유로를 지출해야만 했다.

제품을 만들 때 기능 위주의 블랙박스 테스트만 진행하면 이는 빙산의 일각만 테스트한 것과 같다. 내부 소프트웨어 흐름을 보지 않고 외부로 나와 있는 부분만 테스트한 것이기 때문이다. 소프트웨어가 정상적으로 동작하는 것처럼 보이지만 내부적으로 소프트웨어 흐름을 보면 많은 위험 요소가 존재한다.

이런 위험 요소를 찾아내기 위해서는 사람이 직접 하기보다는 체계적인 기준과 툴을 가지고 테스트하는 것이 좋다. 기존의 잠재적인 문제들을 찾아내는 데 도움이 될 것이다. 그러나 이런 부분까지 모두 테스트하려면 그만큼 시간과 비용, 인력 등이 많이 투입돼야 한다. 선택은 독자들의 몫이지만 한 가지는 분명히 알아둬야 한다. 소프트웨어를 제대로 만들지 못해 발생하는 문제에 대해 반드시 대가를 치르게 될 것이란 점이다. 이 때문에 사람의 목숨과 직결되는 자동차나 국방/항공 시장은 소프트웨어 품질을 향상하기 위해 ISO26262 표준을 만들어 준수하게 하거나 자체 무기체계 SW 실무 지침서를 만들어 시행하고 있다.



소프트웨어 테스트

소프트웨어 테스팅 모델은 여러 가지가 있으며, ‘V 모델’이라는 테스팅 모델을 가장 많이 사용한다. V 모델은 소프트웨어 생명주기(SDLC : Software Development Life Cycle)를 모형화한 것으로, 폭포수형(Water-fall)을 네 가지 모델 단계로 정의하고 시스템 검증과 테스트 절차를 강화한 모델이다.



V 모델은 개발 및 테스트 단계를 기준으로 Verification과 Validation으로 나뉜다. Verification의 의미는 “우리가 맞는(right) 제품을 만들고 있는가?”를 검증하는 것이고, Validation은 “우리가 제품을 맞게(right) 만들고 있는가?”를 검증하는 것이다. Verification 작업을 하기 위해서는 제품을 개발하기 전에 요구사항 같은 내용을 문서로 만들게 되고, 이 문서를 토대로 코딩이 이뤄지게 된다. 이렇게 실제로 시스템 동작이 아닌 문서와 코딩같이 정적인 내용을 가지고 검증을 하는 것을 정적 분석(Static Analysis)이라고 한다. 정적 분석은 크게 코딩 규칙과 실행시간 오류를 점검하게 된다. 코딩 규칙은 프로그램 코딩 시 준수해야 할 규칙, 주석 규칙, 프로그램 작성 규칙 등을 점검하고, 실행시간 오류는 프로그램 실행에 영향을 미칠 수 있는 오류를 검출한다. 예를 들어 수행 중 초기화되지 않은 변수 사용이나 NULL Point 에러 등이 여기에 해당한다.



Validation은 소프트웨어 동작이 가능한 형태로 테스트를 수행하여 결과를 얻어내는 것으로, 동적 테스트(Dynamic Test)라고 한다. 동적 테스트의 각 테스트 단계는 Verification 영역에 있는 각 단계의 내용을 기반으로 검증을 수행한다. 단위 테스트 단계에서는 구현에 대한 신뢰성 검증을, 통합 테스트는 상세 설계에 대한 유효성 검증을, 시스템 테스트 단계에서는 설계에 대한 검증을, 그리고 인수 테스트 단계에서는 사용자 요구 사항에 대한 만족도를 중심으로 테스트를 수행한다.





동적 테스트에서 단위 테스트와 통합 테스트의 경우 소스 코드의 품질을 평가할 때 코드 실행률(Code Coverage)을 기반으로 평가한다.



타깃 기반의 동적 커버리지 측정

코드 커버리지(Coverage)란 소프트웨어 테스트를 논할 때 얼마나 테스트가 충분한가를 나타내는 지표다. 코드 커버리지 종류는 일반적으로 문장 커버리지(Statement Coverage), 분기 커버리지(Branch Coverage), MC/DC 커버리지(Modified Condition/Decision Coverage)를 많이 사용한다.



타깃에 동적 커버리지를 추출하기 위해서는 소스 코드에 Test Point라는 코드를 실행 구문에 삽입해 기존 컴파일러로 다시 컴파일한 후 테스트를 진행해야 한다. 하지만 모든 코드 라인에 Test Point라는 것을 삽입하면 실제 Target에서 정상적으로 동작하지 않을 수 있다. 실행 블록에 대해서만 Test Point를 삽입해 코드 커버리지를 추출할 수 있다.



<리스트 1>과 <리스트 2>는 DT10 툴을 이용해 코드 커버리지를 추출하고자 Test Point를 자동 삽입한 예이다(Decision Coverage 추출 예).

<리스트 1> 원본 소스 코드 void InitStage(){ Stage = 0; Score = 0; Line = LineLimit[Stage]; Speed = StageSpeed[Stage]; for(i = 0 ; i < Rows ; i++){ for(j = 0 ; j < Cols ; j++){ if(j == 0 || j == Cols - 1 || i == Rows - 1) Back[i][j] = 9; else Back[i][j] = 8; } } }



<리스트 2> Test Point가 삽입된 소스 코드 void InitStage( ) { __DtTestPoint( __DtFunc_InitStage, __DtStep_0 ); Stage = 0; Score = 0; Line = LineLimit[Stage]; Speed = StageSpeed[Stage]; for( i = 0; DT_DC_TRUE ; i++ ) { if( !( i < Rows ) ) { /** DT-DC(l.220) **/ __DtTestPoint( __DtFunc_InitStage, __DtStep_1 ); break; } /** DT-DC(l.220) **/ __DtTestPoint( __DtFunc_InitStage, __DtStep_2 ); { /** DT-DC(l.220) **/ for( j = 0; DT_DC_TRUE ; j++ ) { if( !( j < Cols ) ) { /** DT-DC(l.221) **/ __DtTestPoint( __DtFunc_InitStage, __DtStep_3 ); break; } /** DT-DC(l.221) **/ __DtTestPoint( __DtFunc_InitStage, __DtStep_4 ); { /** DT-DC(l.221) **/ if( j == 0 || j == Cols - 1 || i == Rows - 1 ) { /** DT-SC **/ __DtTestPoint( __DtFunc_InitStage, __DtStep_5 ); Back[i][j] = 9; } /** DT-SC **/ else { /** DT-SC **/ __DtTestPoint( __DtFunc_InitStage, __DtStep_6 ); Back[i][j] = 8; } /** DT-SC **/ } /** DT-DC(l.221) **/ } } /** DT-DC(l.220) **/ } }



각 Test Point에 대한 함수는 header 파일에 자동으로 생성되며 함수 원형은 _TP_BusOut 함수가 된다.



<리스트 3> DT10 툴에 의해 자동 생성된 header파일 /* TestPointDisableList ---------------------------------------------------*/ #define?????? __Dt__DtFunc_WinMain__DtStep_0?? /*FuncIn*/? _TP_BusOut(__DtBaseAddress,? 0x0000 ); #define?????? __Dt__DtFunc_WinMain__DtStep_1?? /*while*/??? _TP_BusOut(__DtBaseAddress,? 0x0001 ); #define?????? __Dt__DtFunc_WinMain__DtStep_2?? /*while*/??? _TP_BusOut(__DtBaseAddress,? 0x0002 ); #define?????? __Dt__DtFunc_WinMain__DtStep_3?? /*FuncOut*/???????????????? _TP_BusOut(__DtBaseAddress,? 0x0003 ); #define?????? __Dt__DtFunc_WndProc__DtStep_0? /*FuncIn*/? _TP_BusOut(__DtBaseAddress,? 0x0004 ); #define?????? __Dt__DtFunc_WndProc__DtStep_1? /*switch*/?? _TP_BusOut(__DtBaseAddress,? 0x0005 ); #define?????? __Dt__DtFunc_WndProc__DtStep_2? /*switch*/?? _TP_BusOut(__DtBaseAddress,? 0x0006 ); #define?????? __Dt__DtFunc_WndProc__DtStep_3? /*for*/??????? _TP_BusOut(__DtBaseAddress,? 0x0007 ); #define?????? __Dt__DtFunc_WndProc__DtStep_4? /*for*/??????? _TP_BusOut(__DtBaseAddress,? 0x0008 ); ...



TP_BusOut 함수는 소스 파일의 index 번호와 Test Point의 index 두 가지 인자를 받게 되며, 이 인자를 다양한 방법으로 저장해 결과를 얻을 수 있도록 DT10 툴이 지원하고 있다(Memory, FileSystem, Ethernet, Serial, ASYNC, CAN, SD,GPIO). 또한 DT10 툴은 C, C++, C#, JAVA 언어를 지원하고 시스템의 종속성(dependency)이 없으므로 Embedded, RTOS 및 PC 기반의 애플리케이션에 모두 사용할 수 있다.



<리스트 4> DT10 Driver 샘플(커버리지 전용) void _TP_BusOut( DT_UINT addr, DT_UINT dat ) { DT_UINT count = 0; unsigned char buff[DT_NRMLTP_SIZE]; #if DT_USE_ONETIME_TRACE_MODE DT_UINT key; #endif #if DT_USE_ONETIME_TRACE_MODE key = MAKE_KEY(addr, dat); if (logBitBuf[key/DT_UINT_BIT_SIZE] & (1 < < (key & (DT_UINT_BIT_SIZE - 1)))) { #if DT_ADD_CRITICALSECTION_INFO LeaveCriticalSection(&cs); #endif return; } logBitBuf[key/DT_UINT_BIT_SIZE] |= 1 < < (key & (DT_UINT_BIT_SIZE - 1)); #endif buff[0] = (unsigned char)((dat) & 0xFF); buff[1] = (unsigned char)((dat >> 8) & 0xFF); buff[2] = (unsigned char)((addr) & 0xFF); buff[3] = (unsigned char)((addr >> 8) & 0xFF); …



그리고 각 Test Point의 반복문 수행 시 overhead가 증가하므로 실행 소프트웨어가 정상적으로 동작하지 않을 수 있다. 이를 위해 각 Test Point의 make key를 만들어 한 번 수행된 Test Point의 경우 재실행될 때 overhead를 줄일 수 있도록 define(#define DT_USE_ONETIME_TRACE_MODE)으로 처리할 수 있다.

GPIO(General Purpose Input Output)의 경우 DATA 4pin과 CS, CLK pin으로 Test Point의 두 인자를 4bit씩 전송해 주면 원하는 결과 값을 볼 수 있다. 다만 PC의 경우 GPIO의 신호를 직접 받을 수 없으므로 다이내믹 트레이서(Dynamic Tracer)를 연결해 USB로 데이터를 전송하게 된다. <그림 9>는 GPIO로 데이터를 전송하는 모습이다.







이렇게 얻어진 데이터를 DT10 툴을 통해 분석하면 타깃 기반 소프트웨어의 동적 커버리지(기본의 StatementCoverage(Block Coverage), Branch Coverage)를 얻을 수 있다. 또 소스 코드에서 Test Point가 수행된 부분과 미수행된 부분을 색상으로 표시해 줌으로써 불필요한 코드가 삽입됐는지 예외 사항 코드인지를 판별할 수 있다.

Test Point를 Ethernet, GPIO, SD, CAN, Serial로 보낼 경우 커버리지를 실시간으로 확인할 수 있어 부족한 테스트 시나리오, 테스트 케이스를 커버리지 리포트(Coverage Report)로 빠르게 확인할 수 있다. 이처럼 타깃 기반의 동적 커버리지(Dynamic Coverage)를 높임으로써 불필요한 코드를 삭제하고 테스트 결과를 확인함으로써 소프트웨어의 품질을 보다 향상할 수 있다.



테스크 및 함수 기반의 성능 측정

요즘 소프트웨어 복잡도가 높아지고 코드 라인이 늘어나면서 성능 이슈가 발견돼도 어느 부분에 코드를 수정해야 하는지 튜닝 포인트를 찾지 못하는 경우가 있다. 이때 소스 코드 함수의 in/out에 Test Point를 삽입하면 함수의 in/out 수행 시간을 확인할 수 있으므로 튜닝 포인트를 쉽게 찾을 수 있다.

DT10의 경우 함수의 in/out에 Test Point를 넣을 수 있도록 기능을 제공하며, 그 결과를 얻을 수 있도록 돕는다. Open OS와 Firmware의 경우 Task의 Context Switching에 Test Point를 삽입하면 함수 in/out뿐 아니라 Task, Thread에 대한 점유율까지 확인할 수 있다.



● Function Exectution Time Report
이 리포트에서는 함수의 in/out에 Test Point가 추가된 내용을 기반으로 함수가 몇 번 호출(Pass count)이 됐는지, 함수의 in/out 수행 시 시간이 얼마나 걸렸는지(min, max, average 값)를 나타낸다. 이 리포트를 통해 수행된 소프트웨어의 함수 단위로 성능을 확인해 볼 수 있고 튜닝 포인트도 찾을 수 있다.





● Function Period Time Report
이 리포트에서는 Timer or IST(Interrupt Service Thread) 함수가 주기적으로 호출되는지에 대한 여부를 확인할 수 있으며, 함수의 주기 시간도 확인할 수 있다. 또한, 해당 함수의 Period 값을 정해 주면 사양서에서 벗어난 값들을 확인할 수 있는 것도 이 리포트의 특징이다.







● Process Occupational Ratio Scope
Open OS, RTOS, Firmware의 경우 스케줄러에 Test Point를 삽입하면 Task가 수행되는 도중에 어떤 Task가 점유율이 높은지 확인할 수 있다. 해당 Task에 Test Point를 추가로 삽입하면 어떤 함수에서 로드됐는지도 확인할 수 있다.



<리스트 5> Linux Kernel에 Test Point를 삽입한 예 static inline void context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next) { struct mm_struct *mm, *oldmm; unsigned int dt_tgid; prepare_task_switch(rq, prev, next); dt_tgid = next->mm?next->tgid:0; mm = next->mm; __DtTestPointKernelInfo( __DtFunc_KernelInfo, __DtStep_0, next->pid, dt_tgid, next->comm ); oldmm = prev->active_mm; /* * For paravirt, this is coupled with an exit in switch_to to * combine the page table reload and the switch backend into * one hypercall. */ arch_start_context_switch(prev); … }





● Function Transition Scope
Functoin Transition Scope는 CPU 별 점유율과 Process, Thread 별 함수 내의 Test Point 흐름을 타임라인(time line)에 맞게 출력함으로써 코드 수행 시간의 부하율을 확인할 수 있다. 또한, SMP 환경의 코어(Core) 부하율을 측정해 적절하게 Task들을 조절할 수 있도록 다양한 정보를 제공하고 있다.



● MultiWave Scope
MultiWave Scope는 변수 값의 추이나 하드웨어 신호(디지털 신호 4개, 아날로그 신호 2개)를 같은 타임라인에서 확인할 수 있는 기능을 제공하고 이를 통해 하드웨어와 소프트웨어 상태를 동시에 체크할 수 있다.





마치며

소프트웨어 테스트는 아무리 강조해도 지나침이 없다. 다만, 소프트웨어 테스트를 위해서는 비용과 인력, 시간이 필요하므로 툴을 이용하면 작업 효율성을 높이면서 기본적인 소프트웨어 품질과 성능을 유지할 수 있다. 오류와 버그로 인한 피해를 상당 부분 줄일 수 있을 것이다.



출처 : 마이크로소프트웨어 8월호

제공 : 데이터 전문가 지식포털 DBguide.net