TDD/단위 테스트

[단위 테스트] 좋은 단위 테스트의 4대 요소

로파이 2022. 12. 18. 22:18

좋은 단위 테스트를 작성하기 위한 4대 요소

 

-> 회귀 방지

-> 리팩토링 내성

-> 빠른 피드백

-> 유지 보수성

 

회귀 방지

간단히 말해서 버그를 방지하는 테스트 코드를 작성하는 것이다. 회귀 방지는 코드의 복잡도, 전체 실행되는 코드 양, 도메인의 중요성에 따라 긴밀하게 작성되어야 한다.

 

리팩토링 내성

테스트 코드가 테스트하는 실제 로직을 리팩토링 했을 때, 테스트 결과가 빨간불이 나오면 안된다. 이는 테스트 코드의 품질과 높은 연관이 있다.

 

- 거짓 양성 False Positive

거짓 양성으로 판별되는 테스트 코드는 실제 코드가 기능적으로 정상이지만 테스트 결과는 실패를 반환하는 것을 의미한다.

 

"높은 회귀 방지율"

좋은 단위 테스트는 기능 고장이 실제 배포 전에 테스트를 통해 실패가 되어 미리 방지할 수 있다.

 

"강건한 리팩토링 내성"

리팩토링 내성이 낮은 테스트 코드는 테스트 코드의 신뢰율을 하락시킨다.

 

그렇다면 리팩토링 내성은 왜 중요한 것인가? 

 

거짓 양성을 줄이기

거짓 양성은 양치기 소년이 늑대를 계속 외치는 테스트 케이스이다.

 

-> 개발자들은 주 비지니스 로직을 검증하기 위해 테스트 코드를 작성한다.

-> 규모가 점점 커지면서 확장성있는 코드를 위해 비지니스 로직을 리팩토링하기 시작한다.

-> 잘 통과하던 테스트들이 실패하기 시작한다. 하지만 실제 비지니스 로직 기능은 정상적으로 동작했다.

-> 개발자들은 이러한 케이스들을 무시하기 시작한다. (거짓 양성을 만들어내기 시작한다.)
-> 처음에는 몇 안되던 테스트 케이스 실패 사례들이 점점 늘어나며 정말 고쳐야하는 로직에 대한 실패와 고칠 필요가 없는 로직에 대한 실패에 대해 구분이 안되기 시작한다.

-> 다시 테스트 코드를 전부 손 보거나 실패 케이스를 만들지 않기 위해 리팩토링을 안하기 시작한다.

 

거짓 양성을 유발하는 코드

class Message {
public:
	string Header;
	string Body;
	string Footer;
};

class IRender {
public:
	virtual string Render(Message message) abstract;
};
class MessageRenderer : public IRender {
public:
	vector<IRender*> SubRenders;

	MessageRenderer() {
		SubRenders = {
			new HeaderRenderer(),
			new BodyRenderer(),
			new FooterRenderer()
		};
	}

	virtual string Render(Message message) override {
		stringstream ss;
		for (auto render : SubRenders) {
			ss << render->Render(message);
		}

		return ss.str();
	}
};

TEST(TestMessageRender, 메세지_렌더러의_서브_렌더러_유효함){
	MessageRenderer sut = {};
	auto& subRenderers = sut.SubRenders;

	ASSERT_EQ(3, subRenderers.size());
	ASSERT_TRUE(dynamic_cast<HeaderRenderer*>(subRenderers[0]) != nullptr);
	ASSERT_TRUE(dynamic_cast<BodyRenderer*>(subRenderers[1]) != nullptr);
	ASSERT_TRUE(dynamic_cast<FooterRenderer*>(subRenderers[2]) != nullptr);
}


메시지 렌더러(MessageRenderer)는 IRender를 상속하는 개체를 3개 가지고 있으며 이를 순서대로 Header, Body, Footer Renderer로 제한하는 테스트 코드를 구현하였다.

 

이러한 HTML 양식을 만들어내는 메세지 렌더러의 구성 요소가 일부 바뀌었고 예를 들어 BodyRenderer를 대신하여 BoldRenderer로 대체되었다고 하자. 새로운 메세지 렌더러는 여전히 유효한 HTML 양식을 만들어내지만, 테스트 결과는 빨간불을 낼 것이다.

 

실제 기능적으로 문제가 없지만 테스트 결과가 실패로 나오게 되고 이는 거짓 양성을 의미한다.

 

이는 결국 SUT := MessageRenderer의 세부 구현 사항과 테스트 코드가 강하게 결합되어 나타난 결과이다.

 

거짓 양성을 만들어내는 코드는 프로젝트 초기에는 영향이 미미하나 나중에 급격히 나쁜 영향을 미친다.

 

거짓 양성을 줄이는 코드 만들기 : 무엇을 테스트 할 것인가?

- 위 시나리오의 테스트는 성공하였지만 제대로된 검증을 한다고 이야기하기는 어렵다.

- MessageRenderer의 구현 세부 사항을 모르게 하고 해당 기능을 테스트하는 데 초점을 맞추도록 한다.

TEST(TestMessageRender, 메세지_렌더러의_HTML_결과_유효함) {
	MessageRenderer sut = {};
	Message msg;
	msg.Header = "h";
	msg.Body = "b";
	msg.Footer = "f";

	string html = sut.Render(msg);

	ASSERT_EQ(html, "<h1>h</h1><b>b</b><i>f</i>");
}

 

 

테스트 성능을 나타내는 지표

 

실제 로직 (작동, 고장) -> 테스트결과(통과, 실패)에 따른 전체 분류를 4가지로 나눌 수 있다.

오류 유형
기능
작동
고장
테스트
결과
테스트 통과
올바른 추론 (참 음성)
2종 오류 (거짓 음성)
테스트 실패
1종 오류 (거짓 양성)
올바른 추론 (참 양성)

True Positive : 비정상적인 로직이 테스트를 실패하였다. (이는 True Negative가 될 예정이다.)

True Negative : 정상적인 로직이 테스트를 통과하였다.

False Negative : 비정상적인 로직이 테스트를 통과하였다.

False Positive : 정상적인 로직이 테스트를 실패하였다.

 

정확도 높이기 = 회귀 방지율 높이기 = False Negative를 줄이기

비정상적인 로직이 테스트를 통과하였으므로 테스트를 수정해서 회귀 방지율을 높이도록 한다.

-> 얼마나 버그가 있음을 잘 나타내는가

 

재현율 높이기 = 리팩토링 내성 높이기 = False Positive 줄이기

정상적인 로직이 테스트를 실패하였으므로 테스트를 수정해서 리팩토링 내성을 높이도록 한다.

-> 얼마나 버그가 없음을 잘 나타내는가

 

테스트 정확도, 신호 품질과 연관하여 (SNR : Signal to Noise Ratio)

 

테스트 정확도 = 신호 (버그 수) / 소음 (허위 경보 false alarm 수)

 

결론 : 진짜 버그를 더 잘 잡고 가짜 버그는 안 잡도록 만들자.

 

세번째와 네번째 요소

 

빠른 피드백

테스트 코드가 결과를 나올 때 까지 얼마나 걸리는가.

 

유지 보수성

이해하기 쉬운 그리고 실행 하기 쉬운(외부 의존성이 적은) 테스트 코드를 작성하자.

 

이상적인 테스트 만들기

회귀 방지 vs 리팩토링 내성 vs 빠른 피드백

 

극단적 사례

  회귀 방지 리팩토링 내성 빠른 피드백
종단 테스트 (E2E) 높음 높음 낮음
간단한 테스트 낮음 높음 높음
깨지기 쉬운 테스트 높음 낮음 높음

- 종단 테스트 : 큰 모듈 인터페이스 간(UI, 데이터베이스, 외부 어플리케이션) 테스트

 

리팩토링 내성과 유지 보수성을 크게 하되, 회귀 방지와 빠른 피드백간 상호 배타적 관계에서 적절히 선택해야한다.

 

-> 높은 회귀 방지율은 느린 피드백을 갖는다.

-> 빠른 피드백은 낮은 회귀 방지율을 갖는다.


테스트 피라미드

테스트 수 : 단위 테스트 >> 통합 테스트 >> E2E

높은 회귀율 ~ 빠른 피드백 : (높은 회귀율) E2E >> 통합 테스트 >> 단위 테스트 (빠른 피드백) 

 

결론

좋은 테스트를 작성하기 위해 버그를 잘 잡는 (높은 회귀율) 그리고 거짓 양성을 줄이는(리팩토링 내성) 코드를 작성하자.

거짓 양성을 줄이기 위해서는 SUT 세부 구현사항이 테스트 코드에 나타나서는 안된다.

회귀 방지와 리팩토링 내성은 테스트 정확도를 높인다.

 

 

Unit Testing 단위 테스트, 블라디미르 코리코프 지음
https://www.sandordargo.com/blog/2019/04/24/parameterized-testing-with-gtest#parameterizedtests
https://github.com/jinsnowy/UnitTest_Study