-
단위 테스트1 - 단위 테스트 목표카테고리 없음 2024. 8. 7. 14:01
1. 단위 테스트 목표
1.2 단위 테스트의 목표
테스트코드를 작성하지 않으면 처음에는 발목을 잡을 것이 없으므로 빨리 시작할 수 있다. 아직 잘못된 아키텍처 결정이 없고, 걱정할 만한 코드가 있지도 않다. 그러나 시간이 지나면서 점점더 많은 시간을 들여야 처음에 보여준 것과 같은 정도의 진척을 낼 수 있다. 결국 개발속도가 현저히 느려지고, 심지어 전혀 진행하지 못할 정도로 느려질 수 있다.
개발 속도가 빠르게 감소하는 이러한 현상을 소프트웨어 엔트로피(software entropy)라고도 한다. 엔트로피(시스템 내 무질서도)는 소프트웨어 시스템에도 적용할 수 있는 수학적이고 과학적인 개념이다.
소프트웨어에서 엔트로피는 품질을 떨어뜨리는 코드 형태로 나타난다. 코드베이스에서 무언가를 변경할 때마다 무질서(엔트로피)는 증가한다. 지속적인 정리와 리팩토링 등과 같은 적절한 관리를 하지 않고 방치함녀 시스템이 점점 더 복잡해 지고 무질서 해진다. 하나의 버그를 수정하면 더 많은 버그를 양산하고, 소프트웨어의 한 부분을 수정하면 다른 부분들이 고장난다. 즉, 도미노 현상과 같다 결국 코드베이스를 신뢰할 수 없게 된다. 그리고 최악의 경우 안정되게 복구하는 것은 어렵다.
테스트는 이러한 경향을 뒤집을 수 있다. 테스트는 안전망 역할을 하며 대부분은 회귀(regression)에 대한 보험을 제공하는 도구할 수 있다. 테스트는 새로운 기능을 도입하거나 새로운 요구 사항에 더 잘 맞게 리팩토링한 후에도 기존 기능이 잘 동작하는지 확인하는데 도움이된다.
1.2.1 좋은 테스트와 좋지 않은 테스트를 가르는 요인
단위 테스트가 프로젝트 성자에 도움이 되는 것은 맞지만, 테스트를 작성하는 것만으로는 충분하지 않다. 잘못 작성한 테스트는 여전히 같은 결과를 낳는다.
모든 테스트를 작성할 필요는 없다. 일부 테스트는 아주 중요하고 소프트웨어 품질에 매우 많은 기여를 한다. 그 밖에 다른 테스트는 그렇지 않다. 잘못된 경고가 발생하고, 회귀 오류를 알아내는데 도움이되지 않으며, 유지 보수가 어렵고 느리다. 프로젝트에 도움이 되는지 여부를 명확하게 파악하지 않고 단위 테스트를 작성하는데만 빠져들기 쉽다.
단지 프로젝트에 테스트를 더 많이 쏟아내도 단위 테스트의 목표를 달성할 수 없다. 테스트의 가치와 유지비용을 모두 고려해야한다.
- 기반 코드를 리팩터링할 때 테스트도 리팩토링하자
- 각 코드 변경 시 테스트를 실행하자
- 테스트가 잘못된 경고를 발생시킬 경우 처리하자
- 기반 코드가 어떻게 동작하는지 이해하려고 할 때는 테스트를 읽는데 시간을 투자하라
높은 유지 보수 비용으로 인해 순가치가 0에 가깝거나 심지어 0보다 작은 테스트를 만들기 쉽다. 지속 가능한 프로젝트 성장을 위해서는 고품질 테스트에만 집중해야 한다. 고품질 테스트만이 테스트 스위트에 남을 마난 테스트 유형이다.
제품 코드 대 테스트 코드
사람들은 종종 제품 코드와 테스트 코드가 다르다고 생가간다. 테스트는 제품 코드에 추가 된 것으로 간주되며 소유 비용이 없다. 또한 사람들은 종종 테스트가 많으면 많을수록 좋다고 생각한다. 하지만 그렇지 않다. 코드는 자산이 아니라 책임이다. 코드가 더 많아질수록, 소프트웨어 내의 잠재적인 버그에 노출되는 표면적이 더 넓지고 프로젝트 유지비가 증가한다. 따라서 가능한 한 적은 코드로 문제를 해결하는 것이 좋다.
테스트도 역시 코드다. 특정 문제를 해결하는 것, 즉 애플리케이션의 정확성을 보장하는 것을 목표로하는 코드베이스의 일부로 봐야 한다. 다른 코드와 마찬가지로 단위테스트도 버그에 취약하고 유지보수가 필요하다.
1.3 테스트 스위트 품질 측정을 위한 커버리지 지표
가장 널리 사용되는 두 가지 커버리지 지표(코드 커버리지와 분기 커버리지)를 어떻게 계산하고 어떻게 사용하는지 살펴보고 관련된 문제점도 알아본다. 프로그래머가 특정 커버리지 숫자를 목표로 하는 것이 해로운 이유와 테스트 스위트 품질을 결정하는 데 커버리지 지표에 의존할 수 없는 이유를 알아본다.
커버리지 지표는 각기 다른 유형이 있으며, 테스트 스위트의 품질을 평가하는데 자주 사용된다. 일반적으로 커버리지 숫자가 높을 수록좋다. 안타깝게도 그렇게 간단하지 만은 않다. 커버리지 지표는 중요한 피드백을 주더라도 테스트 스위트 품질을 효과적으로 측정하는데 사용 될 수 없다. 코드를 단위 테스트하는 것과 같은 상황이다. 즉, 커버리지 지표는 괜찮은 부정 지표이지 좋지 않은 긍정 지표다.
부정지표(Negative Indicator)
부정지표는 시스템 문제나 결함을 강조하는 지표이다. 주로 성능저하, 오류, 실패, 결함 등의 부정적 요소를 측정한다. 즉 시스템의 문제점을 발견하고 개선할 수 있는 기회를 제공한다.
장점 - 시스템의 취약점을 발견하고 해결하는데 유용
단점 - 문제를 해결하는 데 집중하다 보기, 성과를 높이는 데 필요한 긍정적인 행동을 간과할 수 있음.
- 버그 수: 소프트웨어에서 발견괸 버그의 수.
- 다운타임 : 시스템이 비정상적으로 작동하지 않는 시간
- 고객 불만 건수 : 고객이 제출한 불만 또는 불평 건수.
- 취소율 : 구독 서비스에서 고객이 구족을 취소하는 비율
긍정지표(Positive Indicatior)
긍정지표는 시스템 성공이나 성과를 강조하는 지표이다. 주로 성과 향상, 목표 달성, 만족도 등의 긍정적인 요소를 측정한다. 즉 시스쳄의 성과를 평가하고, 잘된 부분을 강화하며, 동기 부여를 제공한다.
- 품질 지수 : 제품이나 서비스의 품질을 평가한 지표.
- 성과 목표 달성률 : 설정한 목표를 달성한 비율.
- 신규 고객 수: 일정 기간동안 새로 확보한 고객의 수.코드 커버리지가 너무 적을 때는(예를 들면, 10%) 테스트가 충분하지 않다는 좋은 증거다.(낮은 지수일 때는 어느 정도 확정적으로 문제점을 들어낸다.) 그러나 반대의 경우는 그렇지 못하다. 100% 커버리지라고 해서 반드시 양질의 테스트 스위트라고 보장하지 않는다. 높은 커버리지의 테스트 스위트도 품질이 떨어질 수 있다.
1.3.1 코드 커버리지 지표에 대한 이해
우선 가장 많이 사용되는 커버리지 지표로 코드 커버리지(code coverage)가 있으며, 테스트 커버리지(test coverage)로도 알려져있다. 위 그림과 같이 이 지표는 하나 이상의 테스트로 실행된 코드 라인 수와 제품 코드베이스의 전체 라인 수의 비율을 나타낸다.
예제 1.1 예제 메서드를 부분적으로 다루는 테스트
v1 coverage.ts
isStringLong(input: string) { if (input.length > 5) //테스트가 다루지 않는 영역 return true; return false; }
v1 coverage.spec.ts
it('test isStringLong', () => { const result = controller.isStringLong('abc'); expect(result).toEqual(false); });
여기서 코드 커버리지를 쉽게 계산할 수 있다. 메서드 전체 라인 수는(중괄호를 포함해) 5이다. 테스트가 실행하는 라인수는 3이다. 테스트는 ture를 반환하는 구문을 제외한 모든 코드 라인을 통과한다. 따라서 코드 커버리지는 3/5 = 0.6 =60%이다. 이제 메서드를 리팩토링하고 불필요한 if문을 한 줄로 처리하면 어떻게 될까?
v2 coverage.ts
isStringLong(input: string) { return input.length > 5; }
v1 coverage.spec.ts
it('test isStringLong', () => { const result = controller.isStringLong('abc'); expect(result).toEqual(false); });
코드 커버리지 숫자가 바뀌었는가? 그렇다. 테스트는 이제 코드 세 줄(반환문과 중괄호 두개)을 모두 점검하기 때무에 코드 커버리지가 100%로 증가했다.
하지만 리팩토링으로 테스트 스위트를 개선했는가? 물론 그렇지 않다. 단지 메서드 내 코드를 바꿨을 뿐이다. 이 테스트가 검증하는 결과 개수는 여전히 같다.
이 간단한 예제는 커버리지 숫자가 얼마나 허상같은 것 인지를 보여준다. 코드가 작을 수록 테스트 커버리지 지표는 더 좋아진다. 코드를 더 짧게 해도 테스트 스위트의 가치나 코드베이스의 유지 보수성이 변경되지 않는다.
1.3.2 분기 커버리지 지표에 대한 이해
또 다른 커버리지 지표는 분기 커버리지(branch coverage)다. 분기 커버리지는 코드 커버리지의 단점을 극복하는 데 도움이 되므로 코드 커버리지 보다 더 정확한 결과를 제공한다. 분기 커버리지 지표는 원시 코드 라이 수를 사용하는대신 if문과 switch문과 같은 제어 구조에 중점을 둔다. 위 그림과 같이 테스트 스위트 내 하나 이상의 테스트가 통과하는 제어 구조의 수를 나타낸다.
분기 커버리지 지표를 계산하려면 코드베이스에서 모든 가능한 분기를 합산하고 그중 테스트가 얼마나 많이 실행되는지 확인해야한다.
v2 coverage.ts
isStringLong(input: string) { return input.length > 5; }
v1 coverage.spec.ts
it('test isStringLong', () => { const result = controller.isStringLong('abc'); expect(result).toEqual(false); });
isStringLong 메서드에 두 개의 분기가 있는데, 하나는 문자열 인수의 길이가 다섯자를 초과하는 상황에 대한 것이고 다른 하나는 그렇지 않은 경우다. 테스트는 이런 분기 중 하나에 대해서만 적용되므로 분기 커버리지 지표는 1/2 = 0.5 = 50% 이전과 같이 if 문을 사용하든 더 짧은 표기법을 사용하든, 테스트 코드는 어떻게 작성해도 상관없다. 분기 커버리지 지표는 분기 개수만 다루며, 해당 분기를 구현하는데 코드가 얼마나 필요하지 고려하지 않는다.
위 그림은 분기 커버리지 지표를 시각화하는 유용한 방법을 보여준다. 테스트 코드가 취할 수 있는 가능한 경로를 그래프로 모두 표시하고 그 중 얼마나 통과했는지 알 수 있다. IsStringLong은 두 개의 경로가 있으며, 테스트는 그중 하나만 수행한다.
1.3.3 커버리지 지표에 관한 문제점
분기 커버리지로 코드 커버리지보다 더 나은 결과를 얻을 수 있지만, 테스트 스위트의 품질을 결정하는데 어떤 커버리지 지표도 의존할 수 없는 이유는 다음과 같다.
- 테스트 대상 시스템의 모든 가능한 결과를 검증한다고 보장할 수 없다.
- 외부 라이브러리의 코드 경로를 고려할 수 있는 커버리지 지표는 없다.
➡️가능한 모든 결과를 검증한다고 보증할 수 없음
단지 코드 경로를 통과하는 것이 아니라 단위 테스트에는 반드시 적절한 검증이 있어야 한다. 다시 말해, 테스트 대상 시스템이 낸 결과가 정확히 예상하는 결과인지 확인해야한다. 더구나 결과가 여러 개 있을 수 있다. 따라서 커버리지 지표가 의미가 있으려면, 모든 측정 지표를 검증해야한다.
예제1.2 마지막 결과를 기록하는 isStringLong 버전
v3 coverage.ts
public wasLastStringLong: boolean; isStringLong(input: string) { const result = input.length > 5; //첫 번째 결과 this.wasLastStringLong = result; //두 번째 결과 return result; }
v1 coverage.spec.ts
it('test isStringLong', () => { const result = controller.isStringLong('abc'); expect(result).toEqual(false); //두 번째 결과만 검증 });
IsStringLong 메서드에는 이제 값을 반환하는 명시적인 결과와 class의 속성(멤버변수)새로운 값을 대입하는 암묵적인 결과가 있다. 보다시피 커버리지 지표는 기반 코드를 모두 테스트했다고 보장할 수 없으며 일부 실행된 것만 보장한다.
이렇게 결과를 부분적으로 테스트한 것보다 더 극단적인 상황은 검증이 전혀 없는 테스트의 경우다. 다음은 검증이 전혀없는 예제이다.
예제1.3 검증이 없는 테스트는 언제나 통과한다.
v2 coverage.spec.ts
it('No verification isStringLong', () => { const result1 = controller.isStringLong('abc'); //true반환 const result2 = controller.isStringLong('abcdef'); //false 반환 });
위 테스트에서 코드 커버리지와 분기 커버리지가 둘 다 100% 나타내고 있다 그러나 아무것도 검증하지 않기 때무에 전혀 쓸모 없다.
➡️외부 라이브러리의 코드 경로를 고려할 수 없음
두 번째 문제는 코든 커버리지 지표가 테스트 대상 시스템이 메서드를 호출할 때 외부 라이브러리가 통과하는 코드 경로를 고려하지 않는다는 것이다. 다음 예를 보자.
실행코드
parseToNumber(input: string) { return +input; }
테스트코드
it('test parseToNumber', () => { const result = controller.parseToNumber('5'); expect(result).toEqual(5); });
분기 커버리지 지표는 100%로 표시되며, 테스트는 메서드 결과의 모든 구성 요성를 검증한다. 테스트는 메서드의 모든 구성 요소를 검증한다. 단지 값을 반환하는 한 줄이라 하더라도 단일한 구성 요소이기는 하다.하지만 이 테스트는 완벽하지 않다. javascript 차원에서 +input이 수행하는 코드 경로는 고려하지 않는다.
검증할 구성 요소
1. 반환 값
- 메서드가 예상한 값을 반환하는지 검증한다. 이는 가장 기본적인 검증이다.
- ex) add(2,3)가 5를 반환하는지 확인
2. 사이드 이펙트 검증
- 메서드가 외부 시스템에 미치는 영향을 검증한다.
- ex) 특정 로깅 메서드가 호출되었는지 확인
3. 예외 처리 검증
- 잘못된 입력이나 특정 상황에서 예상한 예외가 발생하는 지 확인한다.
- ex) 음수를 입금하려 할 때 예외가 발생하는지 확인
위 그림과 같이 간단한 메서드에도 꽤 많은 코드 경로가 존재한다. 빌트인(built-in)정수 타입에는 메서드의 입력 매개변수를 변경하면 다른 결과로 이어질 수 있고 테스트로 부터 숨어 있는 분기가 많다. 다음은 정수로 변환할 수 없는 몇 가지 가능한 인수다.
- 널 null 값
- 빈 문자열
- "정수가 아님"
- 너무 긴 문자열
수많은 예외 상황edge case에 빠질 수 있지만, 테스트에서 모든 예외 상황을 다루는지 확인할 방법이 없다. 이는 커버리지 지표가 외부 라이브러리 코드 경로를 고려해야 한다는 것이 아니라(고려하면 안된다.)해당 지표로는 단위 테스트가 얼마나 좋은지 나쁜지를 판단할 수 없다는 것을 보여준다. 결국 커버리지 지표로 테스트가 철저한지 또는 테스트가 충분한지 알 수 는 없다.
1.3.4 특정 커버리지 숫자를 목표로 하기
`100%, 90%, 심지어 중간 정도인 70%까지 특정 커버리지 숫자를 목표로 삼기 시작하면 위험 영역으로 이어질 수 있다. 커버리지 지표를 보는 가장 좋은 방법은 지표 그 자체로 보는 것이며, 목표로 여겨서는 안 된다.
커버리지 숫자가 낮으면(예:60%미만)문제 징후라고 할 수 있다. 코드베이스에 테스트되지 않은 코드가 많다는 뜻이다. 그러나 높은 숫자도 별 의미는 없다. 그러므로 코드 커버리지를 측정하는 것은 품질 테스트 스위트로 가는 첫걸음일 뿐이다.
1.4 무엇이 성공적인 테스트 스위트를 만드는가.
- 개발 주기에 통합되어 있다.
- 코드베이스에서 가장 중요한 부분만을 대상으로 한다.
- 최소한의 유지비로 최대의 가치를 끌어낸다.
1.4.1 개발 주기에 통합돼 있음
자동화된 테스트를 할 수 있는 방법은 끊임없이 하는 것뿐이다. 모든 테스트는 개발 주기에 통합돼야 한다. 이상적으로는 코드가 변경될 때마다 아무리 작은 것이라도 실행해야 한다.
1.4.2 코드베이스에 가장 주요한 부분만을 대상을 함
모든 테스트가 똑같이 작성되지 않는 것 처럼 단위 테스트 측면에서 코드베이스의 모든 부분에 똑같이 주목할 필요는 없다. 테스트가 주는 가치는 테스트 구조뿐만 아니라 검증하는 코드에도 있다.
시스템에 가장 중요한 부분에 단위 테스트 노력을 기울이고, 다른 부분은 간략하게 또는 간접적으로 하는 것이 좋다. 대부분의 애플리케이션에서 가장 중요한 부분은 비즈니스 로직(도메인 모델)이 있는 부분이다. 비즈니스 로직 테스트가 시간 투자 대비 최고의 수익을 낼 수 있다.
다른 모든 부분은 세가지 범주로 나눌 수 있다.
- 인프라 코드
- 데이터베이스나 서드파티 시스템과 같은 외부 서비스 및 종속성
- 모든 것을 하나로 묶는 코드
그러나 이 중 일부는 단위 테스트를 철저히 해야 할 수 있다. 예를 들어 인프라 코드에 복잡하고 중요한 알고리즘이 있을 수 있으므로, 테스트를 많이 하는 것이 좋다. 그러나 일반적으로 도메인 모델에 관심을 더 많이 갖는 것이 옳다.
통합 테스트와 같이 일부 테스트는 도메인 모델을 넘어 코드베이스의 중요하지 않으 부분을 포함해서 코드베이스의 중요하지 않은 부분을 포함해 시스템이 전체적으로 어떻게 작동하는지 확인할 수 있다. 이것도 괜찮다. 그러나 초점은 도메인 모델에 머물러 있어야 한다.
이 지침을 따르려면 도메인 모델을 코드베이스 중 중요하지 않은 부분과 분리해야 한다. 도메인 모델을 다른 애플리케이션 문제와 분리해야 단위 테스트에 대한 노력을 도메인 모델에만 집중할 수 있다.
🍯도메인 모델을 다른 애플리케이션 문제와 분리
소프드웨어 개발에서 비지니스 로직(도메인 로직)을 애플리케이션의 다른 관심사들(인터페이스, 데이터베이스 액세스, 인프라스트럭처)로부터 분리하는 것을 의미한다.
1.4.3 최소 유지비로 최대 가치를 끌어낸다.
단위 테스트에서 가장 어려운 부분은 최소 유지비로 최대 가치를 달성하는 것이다. 테스트를 빌드 시스템에 통합하는 것만으로 충분하지 않으며 , 도메인 모델에 높은 테스트 커버리지를 유지하는 것도 충분하지 않다. 또한 가치가 유지비를 상회하는 테스트만 스위트에 유지하는 것이 중요하다.
이 마지막 속성은 두 가지로 나눌 수 있다.
- 가치가 있는 테스트(더 나아가, 가치가 낮은 테스트) 식별하기
- 가치가 있는 테스트 작성하기
이런한 기술은 비슷해 보일비 모르지만, 선척적으로 다르다. 가치가 높은 테스트를 식별하려면 기준들 frame of reference이 필요하다. 반면 가치 있는 테스트를 작성하려면 코드설계 기술도 알아야 한다. 단위 테스트와 기반 코드는 서로 얽혀 있으므로 코드베이스에 노력을 많이 기울이지 않으면 가치있는 테스트를 만들 수 없다. 마치 좋은 곡을 식별하는 것과 작곡할 수 있는 것의 차이로 볼 수 있다.
[출처 - Unit Testing, 저 블라디미르 코리코프]
http://www.acornpub.co.kr/book/unit-testing