단위 테스트 현황

  • 지난 20년간 단위 테스트의 적용은 매우 일반적인 상황이었음
  • 대부분의 프로그래머는 단위 테스트를 실천하고 중요성을 알고 있다
    • (백엔드 개발에서)는 더 이상 단위 테스트 작성이 논쟁의 여지가 될 수 없음

테스트 코드의 비율

  • 대부분의 경우 1:1 에서 1:3, 혹은 비율이 훨씬 높아서 1:10 가까이 되는 경우도 존재하곤 한다

논쟁의 지점

  • 단위 테스트를 작성해야 하는가는 더 이상 논쟁의 지점이 될 수 없다. 작성해야 하기 때문
  • 그렇다면 좋은 단위 테스트를 작성하는 것은 어떤 의미인가? 로 논점이 변경되었음

많은 프로젝트에는 자동화된 테스트도 존재하고, CI 를 통하여 주기적 빌드를 하게 된다. 그리고 많은 양의 테스트가 주기적으로 실행되곤 한다. 그러나, 테스트를 해도 개발자가 원하는 결과를 얻지 못하는 경우가 많다.

원하는 결과를 얻지 못한다는 것은?

  • 구현된 기능에는 많은 버그가 존재하고, 도움이 될 것이라고 생각하는 단위 테스트는 도움이 되지 못함
    • 좋지 않은 테스트는 오히려 상황을 악화시킨다

단위 테스트의 목표

  • 단위 테스트 활동이 더 나은 설계로 이어지는가?
    • 일반적으로 더 나은 설계로 이어지나, 이것이 단위 테스트의 주된 목적은 아니다
    • 왜냐하면, 단위 테스트를 짜기 어려운 코드는 보통은 좋지 못한 코드이지만
    • 반대로 단위 테스트를 짜기 쉬운 코드라고 해도 설계가 좋은 코드 라고 볼 수 없기 때문

단위 테스트의 목표는 무엇인가?

  • 소프트웨어 프로그램의 지속 가능한 성장을 가능하게 하는 것
  • 프로젝트는 초기에는 쉽게 성장할 수 있다. 그러나, 시간이 지나면서 이렇게 성장하기는 훨씬 힘들다
  • 프로젝트가 점차 진척됨에 따라, 테스트가 없는 코드는 점점 더 많은 시간을 들여야 같은 기능을 개발할 수 있다
    • 심지어, 진척도가 0이 되버릴 수도 있음

소프트웨어 엔트로피

  • 소프트웨어 복잡도가 증가함에 따라 개발 속도가 감소하는 현상을 소프트웨어 엔트로피라고 함
  • 소프트웨어에서는 품질을 떨어뜨리는 코드 형태로 나타난다
    • 무언가를 바꾸면, 엔트로피가 증가한다
    • 리펙토링과 지속적인 정리를 하지 않고 두면, 시스템이 점점 더 복잡해지고 무질서해진다

테스트는 안정망 역할을 하며, 대부분에 리그레이션(특정 사건 이후에 기능이 의도한 대로 동작하지 않는 경우. 소프트웨어에서는 버그와 리그레이션은 거의 동의어)에 대해 보험을 제공하는 도구라 할 수 있다

좋은 테스트와 좋지 않은 테스트

  • 테스트가 잘못 작성된 프로젝트는 초반에는 테스트가 있는 프로젝트와 비슷하다
  • 그러나, 결국에는 테스트가 없는 것과 비슷한 상황에 빠짐
  • 일부 테스트는 아주 중요하고 소프트웨어 품질에 많은 기여를 한다
  • 그러나, 그 밖에 다른 테스트는 그렇지 않다
    • 버그 발생을 해결하는 데 도움이 되지 않고
    • 잘못된 경고가 발생하며
    • 유지보수가 어렵고 아주 느리다

테스트 유지 비용

  • 테스트가 많다고 무언가 좋아지는 게 아님
  • 테스트 유지 비용 또한 테스트에 큰 영향을 미친다
    • 기반 코드를 리펙토링 할 때 테스트 코드 리펙토링
    • 각 코드 변경 시에 테스트 실행
    • 테스트가 잘못된 경고 발생
    • 기반 코드가 어떻게 동작하는지 이해하기 위해 테스트를 읽는 시간

테스트가 많을수록 좋은가?

  • 그렇지 않다
    • 사람들은 종종 프로덕션 코드와 테스트 코드가 다르다고 생각한다
    • 하지만 코드는 자산 이 아니라 책임 이기 때문에 코드가 많아지면 많아질수록 잠재적인 버그 요인은 상승한다
  • 단위 테스트 또한 버그에 취약하고 유지보수가 필요하다

테스트 커버리지 지표

  • 두 가지 지표를 넓게 사용함
    • 코드 커버리지와 분기 커버리지
  • 일반적으로는 커버리지 지표가 높을수록 좋다
  • 단 이것 또한 단위 테스트와 비슷하다
    • 커버리지 지표가 너무 낮은 경우에는 테스트가 충분하지 않다 는 좋은 증거
    • 그러나 100% 커버리지 지표라고 해도 반드시 양질의 테스트를 보장하지 않는다

코드 커버리지

  • 코드 커버리지 = 실행 코드 라인 수 / 전체 라인 수
public static bool IsStringLong(string input) {
	if(input.Length > 5) 
		return true
 
	return false
}

이와 같은 코드가 있고 테스트가 있을 때

public void Test() {
	bool result = IsStringLong("abc");
	Assert.Equal(false, result);
}

전체 라인 수는 5, 위의 테스트는 true 를 반환하는 구문을 제외한 라인을 모두 테스트한다. 따라서 4/5 = 80% 이다

이제 메서드를 리펙터링하면 어떻게 될까?

public static bool IsStringLong(string input) {
	return input.Length > 5;
}

이렇게 변경 시 커버리지는 100%가 된다

  • 리펙터링으로 테스트 스위트를 개선했는가? No
  • 커버리지가 증가하였는가? Yes

코드가 적으면 테스트 커버리지는 증가하는데, 이는 애플리케이션의 유지보수성을 변경하지 않는다

분기 커버리지

  • 분기 커버리지 = 통과 분기 / 전체 분기 수
public static bool IsStringLong(string input) {
	return input.Length > 5;
}
 
public void Test() {
	bool result = IsStringLong("abc");
	Assert.Equal(false, result);
}
  • 위 테스트 코드의 분기 커버리지는 1/2 로 50%이다
  • 그러나 위의 지표 또한 높다고 반드시 올바른 테스트라고 말할 수 없다

테스트 대상 시스템의 모든 결과를 검증한다 볼 수 없음

public static bool WasLastStringLong {get; private set;}
 
public static bool IsStringLong(string input) {
	bool result = input.Length > 5;
	WasLastStringLong = result; // 첫 번째 결과
	return result; // 두 번째 결과
}
 
public void Test() {
	bool result = IsStringLong("abc");
	Assert.Equal(false, result); // 두 번째 결과만 검증
}
  • IsStringLong 메서드는 값을 반환 + 속성에 새로운 값을 쓰는 결과가 있음
  • WasLastStringLong 를 검증하지 않더라도 코드 커버리지와 브랜치 커버리지는 같음
public void Test() {
	bool result1 = IsStringLong("abc");
	bool result2 = IsStringLong("abcdef");
}

이 테스트는 코드 커버리지와 분기 커버리지가 모두 100% 지만, 아무 의미가 없다

그렇다면, 테스트 코드 대상에 대한 결과를 철저히 검증한다면, 테스트 지표는 신뢰할 만한 지표가 될 수 있는가? 또 테스트 스위트 품질을 결정하는 데 사용할 수 있는가?

모든 커버리지 지표가 외부 라이브러리를 통과하는 경우

public static int Parse(string input) {
	return int.Parse(input)
}
 
public void Test() {
	int result = Parse("5");
	Assert.Equal(5, result);
}

위의 테스트는 int.Parse 메서드가 수행하는 코드 경로를 고려하지 않음. 입력으로 다음과 같은 값이 들어온다면

  • null
  • ""
  • “정수가 아님”
  • 너무 긴 문자열

라이브러리의 엣지 케이스에 대한 지표를 검증할 수 없다. 라이브러리의 코드 경로를 고려해야 한다는 이야기가 아님. 커버리지 지표로 테스트가 올바른지 검증할 수 없다는 의미

무엇이 성공적인 테스트 스위트를 만드는가

  • 지표가 아니라면, 무엇으로 테스트 스위트가 성공적인지 검증할 수 있는가?

개발 주기에 통합되어 있다

  • 모든 테스트가 개발 주기에 통합되어 있어야 함
  • 코드가 변경될 때 마다 아무리 작은 것이라도 실행하여야 함

코드베이스에서 가장 중요한 부분을 대상으로 수행

  • 시스템의 가장 중요한 부분에 단위 테스트를 추가한다
    • 보통은 비즈니스 로직(도메인 모델) 이 있는 곳이 코드베이스에서 가장 중요한 곳임
  • 다른 부분은 간략하게, 간접적으로 검증
코드 범주
- 인프라 코드
- 데이터베이스나 서드파티 시스템, API 호출과 같은 외부 서비스 종속성
- 모든 것을 하나로 묶는 코드

인프라 코드가 중요할 수도 있다. 통합 테스트 같이 도메인 모델을 넘어서 시스템이 어떻게 작동하는지 확인할 수도 있다. 그러나 대부분은 도메인 모델을 테스트로 검증하여야 함

최소 유지비로 최대 가치를 끌어냄

  • 가치 있는 테스트를 식별하여야 한다
    • 가치 있는 테스트를 식별하려면 나름의 기준이 필요함
  • 가치 있는 테스트를 작성하여야 한다
    • 나름의 기준과 더하여 코드 설계 기술도 알아야 함