[Advanced C++] 20. Code Test, Code Coverage (Branch Coverage, Loop Coverage), Sementic Error, Detect Error & Handling, Assertion

dev.kelvin·2025년 2월 18일
1

Advanced C++

목록 보기
20/74
post-thumbnail

1. Code Test

Code Test

프로그램을 제작했다면 작성한 코드가 의도대로 잘 동작하는지 검증이 필요하며 테스트가 필요하다 (하나의 케이스 뿐 아니라 범용적인 케이스에서도 잘 동작하는지?)

이를 Software Testing이라고 하며 소프트웨어가 실제 예상대로 잘 동작하는지 검증하는 프로세스이다

우선 중요한 점은 프로그램을 작은 단위로 테스트 하는 것이다

이는 무언가 조립을 할 때 각각 부품을 만들고 테스트를 하는것과 전부 조립한 다음에 테스트 하는것의 차이이다

각 부품의 동작을 테스트하고 조립하면 세부적으로 테스트가 가능하며 원인 파악이 더 쉽다, 하지만 전부 조립 후 테스트를 하면 어디서 문제가 생기는지 알기 힘들다, 이는 프로그램에서도 마찬가지이다

따라서 프로그램은 모듈화가 잘 되어있어야 한다 (클래스, 함수등으로 잘 모듈화 되어 있는 프로그램은 검증이 쉽다)

이러한 테스트 방식을 Unit Testing라고 한다

상황과 프로그래머의 의도에 맞게 테스트 함수를 제작하고 돌려보며 테스트를 할 수 있다 (ex) 모음인지 확인하는 함수, 짝수인지 확인하는 함수 등)

이렇게 디버깅, 테스트에 유용하게 사용할 수 있는것이 assert이다

assert

	#include <cassert>
	assert(bool결과값);

bool결과값이 false이면 크래시를 발생시키고 프로그램을 중단시킨다, 이를 통해 null체크도 가능하며 추후에 디버깅을 도와준다 (무언가 null일때 크래시가 발생하기 때문에 어디서 중단되었는지 확인이 가능함)

꼭 필요한 객체가 없을때 assert를 걸어서 크래시를 내고 뒤의 프로그램에 영향을 주지 않는게 좋은 방식이라고 생각한다

Code Coverage

코드 커버리지는 프로그래머가 만든 테스트코드가 소스 코드를 실행한 비율을 의미한다 (테스트 코드가 소스코드를 얼마나 구석구석 잘 확인하고 지나갔는지)

다음은 Code Coverage의 주요 유형이다

Branch Coverage

branch coverage는 모든 분기가 실행되었는지를 확인하는 백분율이다

	bool isLowerVowel(char c)
    {
        switch (c) // statement 1
        {
        case 'a':
        case 'e':
        case 'i':
        case 'o':
        case 'u':
            return true; // statement 2
        default:
            return false; // statement 3
        }
    }

모든 케이스를 전부 테스트 하려면 2번의 소스코드 실행이 필요하다 (branch coverage가 100%가 되려면 2번이 호출되어야 함)

테스트 시 100%의 branch coverage를 목표로 테스트하는것이 좋다 (예외를 최대한 줄이기 위해서)

Loop Coverage

코드에 Loop이 있을 때 0회, 1회, 2회 반복할 때 제대로 작동하는지 확인하는걸 의미한다
(최소 2회)

추가로 input이 있는 함수의 경우 다양한 입력 카테고리에서 테스트를 해봐야 한다

ex) 정수의 제곱근을 생성하는 함수일 때 0이나 음수로도 테스트를 해봐야 정확한 검증이 가능하다는것, 오버플로우 검증 등

ex) 문자열과 같은 경우 빈 문자열, 공백이 포함된 문자열 등 다양하게 테스트해야함


2. 가장 일반적인 Sementic Error

Sementic Error, Syntax Error

Syntax Error는 C++언어 문법에 맞지 않는 코드를 작성했을때 발생하는 error이고 Semetic Error는 의미상 에러이다 (더 디버깅하기 힘들다)

다음은 대표적인 Sementic Error들이다

조건 논리 오류

가장 흔하게 발생하는 Sementic Error는 조건 논리 오류이다
ex) >를 >= 대신 사용하는 경우와 같은

혹은 ||나 &&를 잘못 사용한 경우등이 있다

Infinite Loop

반복문의 조건이 항상 true가 되어 무한 루프가 발생하는 경우도 대표적인 Semetic Error이다

	while(true)
    {
    
    }
    
    for (unsigned int count{ 5 }; count >= 0; --count)
    {

    }

연산자 우선순위

연산자 우선순위를 착각하여 연산이 의도대로 실행되지 않는 경우이다

	if (!x > y)
    {
    }
    
    //!가 >보다 우선순위가 높다, 우선순위를 잘 판단해서 사용해야 한다
    
    if (!(x > y))
    {
    }
    //이 logic과 아예 다른 logic이 되어버림

실수 정밀도 이슈

	float f{ 0.123456789f };
    std::cout << f << '\n';
    
    //0.123457로 반올림 된다, 이러한 정밀도 이슈로 실수를 ==로 비교하다가 Sementic Error가 쉽게 발생할 수 있다
    
    double d{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 }; // should sum to 1.0

    if (d == 1.0)
        std::cout << "equal\n";
    else
        std::cout << "not equal\n";
        
    //같지 않다고 나온다 -> 마찬가지로 실수의 정밀도 이슈 때문

실수의 연산이 누적될수록 반올림에 의한 오차도 누적되어 더 큰 오류가 발생할 수 있다

또한 정수와 정수, 정수와 실수 나누기에서 값을 잘못 예상하여 오류가 발생할 수 있다

null statement

    char c{};
    std::cin >> c;

    if (c == 'y');     // ;가 붙어 null statement가 됨, TestFunction()이 항상 실행됨
        TestFunction();

또한 { }을 사용하지 않아 에러가 발생할 수 있다

	if()
    	Test1();
        Test2();
        
    //Test1만 if조건 검증 후 실행된다, { }를 사용하지 않았기 때문에 Test2는 조건 검증 없이 무조건 실행된다

3. Detect error and handling

에러 감지, 처리

많은 프로그래머들은 코드를 테스트할 때 행복회로를 돌리며 테스트를 한다 (오류가 없는 케이스만을 가지고 테스트를 함)

이러한 테스트 방식은 굉장히 좋지 않다, 프로그램이 크래시 날 각오를 하고 최대한 넓은 방식으로 테스트를 해야 한다 (방어적 프로그래밍으로 생각할 수 있는 모든 다양한 케이스를 예상하여 프로그래밍 하듯 테스트도 마찬가지이다)

함수에서의 error handling

함수를 사용할 때 다양한 이유로 error가 발생할 수 있다

잘못된 파라미터가 넘어왔거나 함수 본문에서 문제가 생기거나 등등..

Log 남기기

함수 내부에서 일어나면 안되는 일이 발생한다면 (파라미터 값, nullptr 등) 직접 log를 남기는 방식으로 error handling이 가능하다

	void TestFunction(int input)
    {
    	if (input == 0)
        {
        	std::cout << "input is zero! Change input!" << '\n';
        }
        else
        {
        	//실행
        }
    }

함수 호출부에 return값 넘겨서 error handling하기

	bool TestFunction(int input)
    {
    	if (input == 0)
        {
        	std::cout << "input is zero! Change input!" << '\n';
            
            return false;
        }
        else
        {
        	return true;
        }
    }
    
    bool bSuccess = TestFunction(0); //false

이렇게 bool값을 return value로 잡고 해당 함수가 정상적으로 동작했는지 handling할 수 있다

하지만 만약 치명적인 오류를 발생한다면 바로 프로그램 종료를 시키는것도 괜찮은 방법이다

	if (input == 0)
    {
   		std::cout << "input is zero! Change input!" << '\n';
            
        std::exit(1); //1로 비정상 종료
    }

early return

특정 조건을 검사하여 로그처리를 하고 early return으로 빠져나오는 방식으로 error handling이 가능하다

	void TestFunction()
    {
    	if (input == 0)
    	{
   			std::cout << "input is zero! Change input!" << '\n';

			return;
    	}
        else
        {
        	
        }
    }

이때 전제 조건은 함수 본문을 실행하기 전에 꼭 참이어야 하는 조건이다, 함수의 맨 위에 배치하는것이 좋다 (전제 조건이 거짓이면 바로 early return해서 밑의 코드 실행을 하지 않게 하기 위해서)


4. Assert, static_assert

Assertion

Assertion은 잘못된 값을 감지하여 오류 메세지를 출력하고 프로그램을 종료시키는 방법이다
(프로그램 종료 시 std::abort를 통해 종료함)

이때 실패한 표현식, 코드 파일 이름, Assertion LineNumber를 텍스트로 포함한다, 따라서 디버깅하기 굉장히 좋다

단순 early return으로 error handling을 한다면 추후에 문제가 생겼을 때 어디서 문제가 생겼는지 확인하기 힘들다 (최소 log라도 찍어놓는 습관을 들이자)

	#include <cassert>
    
    assert(input != 0); //0이면 false로 나와서 프로그램이 종료된다

#define을 이용하여 사용하는 매크로인 preprocessor macro는 지양하는게 좋지만 assert는 예외로 프로그램 전역에서 사용하는것을 권장한다

assert에는 정확한 의미를 전달하는게 중요하다

	assert(input != 0 && "input is zero");

이렇게 &&로 문자열을 같이 넣어주면 크래시 발생 후 assert log로 활용할 수 있다 (문자열은 true로 나오기 때문에 앞의 조건이 false면 크래시 나는건 동일하다)

unimplemented 코드에서 assert를 사용하는것도 좋은 방식이다
(UE에는 unimplemented가 따로 존재함)

assert 사용 시 주의할 점

assert는 product 단계에서는 절대 발생하면 안된다 (디버그 환경에서만 활성화 되어야 함, assert 호출에 약간의 비용이 발생함)

C++에서는 product 단계에서 assert를 끄는 기능이 제공된다

	#define NDEBUG
    #include ???
    #include ???

#define NDEBUG는 다른 #include 전처리기보다 맨 위에 작성되어야 한다, 이 전처리 매크로가 있으면 assert가 동작하지 않는다 (마찬가지로 preprocessor macro임)

보통의 컴파일러에서 Release모드에서는 자동으로 #define NDEBUG가 포함되어 assert를 비활성화 한다

static_assert

C++에는 assert말고 static_assert도 존재한다, 이는 런타임이 아닌 컴파일 타임에 검사되는 assertion이다

static_assert는 키워드이기 때문에 따로 헤더가 필요하지 않다

	static_assert(조건, 메세지);
    
	static_assert(sizeof(int) >= 6, "int must be at least 4 bytes");

static_assert의 조건은 상수 표현식이어야 한다 (컴파일 타임 평가 -> 런타임 비용 없음)
static_assert는 코드 파일 어디든지 배치할 수 있다 (전역 namespace에도 가능)
static_assert는 Release모드에서 비활성화 되지 않는다

이때 메세지는 optional이다

어차피 assertion은 release모드에서 포함되지 않기 때문에 아낄필요 없이 사용하는것이 좋다

assert로 인해 프로그램이 종료될 때 std::abort()가 호출되기 때문에 데이터 손상이 일어날 수 있다 (데이터 손상 가능성이 없을때만 사용해야 한다)

profile
GameDeveloper🎮 Dev C++, DataStructure, Algorithm, UE5, Assembly🛠, Git/Perforce🌏

0개의 댓글