이 글은 Qitta의 geshi님이 작성한 テストがうまくいかないプロジェクトに捧ぐ、正しいテストの考え方, 2016년 3월 9일 갱신 글을 번역한 것입니다.
이 글의 대상자
- 프로젝트에서 테스트를 작성하고 있다. (작성한 적이 있음)
- 테스트가 중요하다는 것은 알고 있지만 테스트의 덕을 제대로 실감한 적이 없다.
- 결국 수동 테스트에 의존해 버그를 고치고 있다.
시작하면서
저는 테스트의 설계방법과 구현에 관한 지식은 많이 지니고 있었지만 잘 몰랐던 것은 테스트에 대한 사고방식이였습니다.
테스트의 중요성에 대해서 알고 있는 사람은 많이 있을 것이라 생각합니다. 하지만 실제로 테스트의 덕을 보지 못하고 있는 사람도 있을 것이라 생각합니다.
사실, 테스트가 중요하다고 말로만 하는 사람 과 테스트의 중요성을 알고 있는 사람 이 있습니다. 후자의 경우, 일단은 테스트를 작성하는 것이 가능합니다. 하지만, 테스트에 시간을 할애하면 할애할 수록 최종적으로는 수동 테스트에서 버그를 발견하는 것에 의존하고 있는 경우가 많을 것이라 생각합니다.
세간에서는 “테스트를 작성하는 것이 당연하고 테스트는 중요해!”라는 풍조에 있는데 왜 테스트가 중요한 것이라고 실감은 할 수 없는 것일까요?? 어떻게 해야만 테스트가 유효하게 활용되고 있는 프로젝트가 될 수 있는 것일까요?
이번의 기사에서는 그러한 문제에 대하여 잔재주가 아니라 실전에서 “어떻게 사고하는가?”라는 사고회로를 설명하고 싶다고 생각합니다.
예를 들면, 이하와 같은 케이스를 생각해 보겠습니다.
케이스 스터디
class SampleController
def index
# do something
end
end
이 액션 안에서는 A,B,C,D,E,F,G,H,I,J라는 10개의 메소드가 호출되어, 각각(0, 1)을 리턴하고 1024가지의 행동거지가 존재합니다.
여기서, 아래의 상태일 경우에는 정상인 행동거지를 하지 못한다는 레포트가 올라왔습니다.
레포트 1
A -> 0
B -> 1
C -> null
D -> 1
E -> 1
F -> 1
G -> 0
H -> 0
I -> 0
J -> - 2
레포트 2
A -> 0
B -> 1
C -> 0
D -> 1
E -> 1
F -> 1
G -> 0
H -> 0
I -> 0
J -> 1
버그가 재발되지 않도록 하기 위해서는 어떤 테스트를 해야만 하는 걸까요??
여기서, 유닛테스트만을 작성하는 프로젝트에서는 레포트1의 버그를 막는 것은 가능해도 레포트 2의 버그를 막는 것은 어려운 경우가 있겠지요.
이 케이스를 기본으로 하여 “어떤 테스트 케이스를 작성해야만 할까요?”라는 부분에 대해 설명해 보도록 하겠습니다.
전제
갑작스럽게 방법론으로 들어가는 것이 아니라 “애초에 테스트의 목적은 무엇인가?”, “이상적인 테스트란 무엇인가?”, “이상적인 테스트의 문제점은 무엇인가?”라는 점에 대해 해석을 통일하고 싶습니다.
- 테스트의 목적
- 버그를 막는데 어떻게 하는게 적절할까?
- 이상적인 테스트란?
- 이상적인 테스트의 문제점
테스트의 목적
테스트의 목적은
- 버그를 발견하는 것
- 품질을 보증하는 것
- 품질을 개선하는 것
이라고 알려져 있습니다. 이 중에 가장 중요한 목적은 1. 버그를 발견 하는 것입니다.
즉, 극단적으로 말하면 버그가 없는 어플리케이션이 이상적인 것이며, 그 이상의 근쳐로 다가가기 위해 좋은 수단 중의 하나가 테스트라는 것입니다.
버그를 막기 위해서는 어떻게 해야하나?
사양(최종 구조/디자인), 프로그램 설계, 테스트라고 하는 세가지의 영역으로 나눠져 있다고 생각합니다.
사양, 프로그램 설계가 나쁘다면 테스트도 힘들어질 것입니다.
그렇기 때문에 스파게티코드는 리팩토링하고 나서 테스트해 주십시오.
하지만, 이번은 테스트에 대한 이야기 입니다.
이상적인 테스트란?
그러면, 이상적인 테스트란 어떤 테스트인 걸까요?
그것은 모든 유스케이스가 정상적으로 동작하는 것 을 증명하는 테스트입니다.
즉 100가지의 사용방법이 있다면 100가지의 응답이 올바른지 어떤지 테스트한다면 완벽하게 버그를 발견할 수 있습니다.
앞서의 케이스를 이용해 설명해보겠습니다.
케이스 스터디
class SampleController
def index
# do something
end
end
이 액션 중에는 A,B,C,D,E,F,G,H,I,J라고 하는 10개의 메소드가 호출 되어 1024가지의 유스케이스가 존재합니다.
또한, 이런저런 메소드는 모두 (0, 1)을 리턴합니다.
여기서 아래의 상태일 때 버그가 발생한다는 레포트가 올라왔습니다.
A -> 0
B -> 1
C -> 0
D -> 1
E -> 1
F -> 1
G -> 0
H -> 0
I -> 0
J -> 1
회답 (이상적인 경우)
일단, 조합에 따라서 버그가 발생하고 있는 것인가, 개별 메소드가 버그를 발생시키고 있는 것인가 알지 못합니다.
하지만 이번에는 그렇게 된 상황을 무시하고 이상적인 입장에서 테스트 케이스를 생각해 보겠습니다.
이상적인 입장에서는 모든 조합이 정상적인지 테스트합니다.
즉, 2 ^ 10 = 1024가지의 테스트를 한다면 버그가 완벽하게 발견되는 것이 가능할 것입니다.
이상적인 경우의 문제점
유스케이스가 1024가지가 있는 기능이라는 것은 무슨 괴물인가 싶습니다만, 현실적으로는 보통의 어플리케이션에 하나의 액션(기능)에 대하여 100가지의 테스트를 하는 것은 불가능합니다. 또한 복수의 액션(기능)이 결합해간다면 무한의 테스트 케이스가 필요하게 됩니다. 여기서 문제점은 모든 유스케이스를 테스트하는 것은 공수적으로 불가능하다는 것입니다.(당연한 말입니다만..)
현실적인 테스트 설계 방법
이상적인 테스트를 목표로 하면 할 수록 지수함수적으로 공수가 늘어납니다.
그렇기 때문에 소프트웨어 테스트의 세계에서는
얼마나 비용대비이득이 높은 테스트를 작성하는 것이 가능한가?
라는 것이 명제가 되어 왔습니다.
그렇다면 비용대비이득이 높은 테스트는 어떻게하면 작성할 수 있을까요?
그것은 버그가 발생하기 쉬운 코드에 대하여 제대로 테스트 코드를 작성하는 것 입니다.
거꾸로 말하면, 이상적인 테스트로부터 중요하지 않은 조건을 솎아내는 것입니다.
그렇다면 지금부터 어떻게 이상적인 테스트케이스로부터 현실적인 레벨로 솎아내어 가는지 세 가지 방법을 설명하겠습니다.
- 방법1 : 독립된 요소를 솎아낸다.
- 방법2 : 중요도가 낮은 패턴은 테스트를 하지 않는다.
- 방법3 : 통계정보에 기반으로한 조합으로 좁힌다.
- 요약
방법1 : 독립된 요소를 솎아낸다.
이 방법은 테스트를 현실적으로 하는 가장 베이스가 되는 사고방법입니다.
케이스 스터디
class SampleController
def index
# do something
end
end
이 액션 중에는 A,B,C,D,E,F,G,H,I,J라는 10가지의 메소드가 호출되어 1024가지의 유스케이스가 존재합니다.
또한, 각각의 메소드는 모두 (0, 1)을 리턴합니다.
명제1 : 「1024가지 유스케이스는 전부 정상」 이라면 「10개의 메소드는 전부 정상」
명제2 : 「10가지 메소드는 모두 정상」 이라면 「1024가지의 유스케이스는 모두 정상」
명제1과 명제2는 각각 참과 거짓 중 어느 쪽일 일까요?
회답
명제1은 참, 명제2는 거짓입니다. (함수형언어를 사용하면 후자라도 참이 되는 프로그램을 작성할 수 있는 듯 합니다만..)
전자는 210 = 1024가지의 테스트를 할 필요가 있으며,
후자는 2*10 = 20가지의 테스트가 조건입니다.
위의 명제는, 유닛테스트만으로 버그를 발견하기 위해서는 불충분하다 는 것을 증명합니다. 하지만 실제로는 솎아내는 것 만으로도 충분한 경우가 있습니다.
예를 들면, 10개의 메소드 안에 D와 H 메소드가 완전히 독립하여 있는(다른 메소드에 대해 영향을 주지 않음) 경우, D와 H는 0 또는 1을 리턴하는 것이 증명이 되어있는 것 만으로 문제없습니다.
또한, E와 F도 각각 영향을 끼치는 스코프가 좁아 조합 테스트에 필요가 없다고 판단할 수 있습니다.
즉, 유스케이스의 지수는 10에서 6으로 줄어들어 유닛테스트가 4개 증가합니다.
2 ^ 6 + 2 * 4 = 70가지
이렇게 해서 독립해 있는 조합을 솎아내 갑니다.
포인트
독립해 있는 메소드는 유닛테스트로 충분하여 조합의 지수가 줄어든다.
방법2 : 중요도가 낮은 패턴은 테스트하지 않는다.
이것은 이론적으로 결정할 수 있는 것은 아닙니다. 케이스 바이 케이스입니다만 사고방법을 알고 있는 것으로 오차는 허용되는 범위로 들어가겠죠.
케이스 스터디
class SampleController
def index
# do something
end
end
이 액션 내에는 A,B,C,D,E,F,G,H,I,J라는 10개의 메소드가 호출되어 1024가지의 유스케이스가 존재합니다.
또한, A라는 메소드는 라이브러리에서 제공되어있는 인증기능입니다. 인증된 경우에는 1, 인증되지 않는 경우는 0을 리턴합니다.
また、Aというメソッドはライブラリーで提供されている認証機能です。認証の場合1 認証でない場合0を返します。
F라는 메소드는 ‘red’, ‘green’이라는 값을 리턴하여 버튼의 색을 바꿉니다.
H라는 메소드는 사용자가 입력했던 정보로부터 플랜A오브젝트, 플랜B오브젝트를 리턴합니다.
이 중에는 테스트하는 것이 좋은 케이스와 테스트하지 않아도 되는 케이스라고 하는 서열이 존재할 터입니다. 그렇다면 어떻게 테스트의 우선순위를 결정해야하는 것일까요??
회답
(중요도) × (버그유입리스크) 라는 매트릭스로 판단합니다.
※ 제3회:테스트항목의 범위 축소:무게를 제는 것은 필히 수치로 표현하자
라고 하는 기사에서 소프트웨어의 중진분들이 의견을 기술하고 있습니다.
중요도라는 것은 사용자적으로 중요, 영향범위가 큰 것 등의 요소가 있습니다.
버그유입리스크란 사양적으로 소스코드적으로 복잡한 곳할 수록 커집니다.(결합도가 높은 곳)
버그유입리스크는 매트릭스해석을 하여 어느정도는 정량적으로 판단하는 것이 가능하기도 합니다. (매트릭스 해석에 대해서는 추후에..)
즉, 이번의 케이스라면 A는 신용할 수 있는 라이브러리로 제공되기 때문에, 버그가 발생할 리스크는 적다.
F영향범위가 좁아지고 사용자적으로도 그렇게까지 중요하진 않다.
H는 어플리케이션의 가치 그 자체로써 로직도 복잡하다.
즉
이러한 매트릭스가 되어 A는 유닛테스트를 할 필요가 없고 F는 조합 패턴에 넣을 필요가 없다고 판단할 수 있습니다.
포인트
(중요도) × (버그유입리스크)를 고려해 유닛테스트가 필요없는 코드, 조합의 패턴에 넣을 필요가 없는 개소를 솎아낸다.
방법3 : 통계정보에 기반한 조합으로 좁힌다.
여기까지 “어떤 테스트케이스를 솎아내는 것이 좋은가?”라고 하는 베이스가 되는 사고방식을 알게 되었습니다.
하지만 인간의 힘으로는 하나의 기능에 대해서 수십개의 테스트케이스조차 빡샌 곳도 있습니다.
여기서 테스트엔지니어 분들은, “어떤 때에 버그가 발생하는가?”라는 것을 통계정보라고 하였습니다.
그러자 “2파라메터 사이까지로 발생한 결함이 7~9할을 차지한다.”” 라고 하는 통계결과가 발견되었습니다. 이 데이터를 기초로 해서 고려된 테스트 설계방법이 페어와이즈법(pairwise) 라고 불리우는 방법입니다.
케이스 스터디
class SampleController
def index
# do something
end
end
이 액션 중에는 A,B,C,D라는 3가지의 메소드가 호출되어 8가지의 유스케이스가 존재합니다.
페어와이즈법이란 아래와 같습니다.
조합테스트기법의 한가지인 올페어(페어와이즈)법에서는, 모든 파라메터에 대하여 적어도 2파라미터사이에서의 값의 조합이 망라되어있듯이 테스트패턴을 작성합니다.
올페어(페어와이즈)법 (일본어)
라고 적혀있습니다.
즉, 2개의 메소드의 조합이 모두 망라되어있다면 문제 없습니다.
예를 들면 A,B,C,D라고 하는 메소드를 페어와이즈법으로 테스트케이스를 생각해보면 아래와 같이 됩니다.
A B C D
1 0 0 0
1 1 1 1
0 1 0 1
0 0 1 1
0 1 1 0
페어와이즈를 작성하는 툴이 존재하는 듯 하여 이를 사용해 보는 것도 좋을지도 모르겠습니다.
조합테스트케이스 생성툴 PictMaster와 소프트웨어 테스트의 화제(일본어)
요약
방법1, 독립해있는 패턴을 찾아봅시다.
방법2, 정말로 테스트해야만 하는지 매트릭스로 생각해봅시다.
방법3, 현명하게 통계정보에 기반으로해 조합의 수를 줄여봅시다.
그렇게 한다면 현실적인 조합이 될 거라고 생각합니다.
위의 실천으로 테스트를 작성할 때에 알아둔다면 좋은 지식
이상으로 베이스가 되는 사고방식을 몸에 익히는 것이 가능하였습니다.
하지만 지금까지의 케이스는 어느정도 이상화된 모델을 베이스로 했던 케이스 스터디입니다.
지금까지의 케이스로는 조합이 매우 명확했습니다.
하지만 현장의 코드에서는 조합의 수는 사람에 따라서 다르다는 것이 발견되기도 합니다.
또한 사람에 따라서 “어떤 코드에 버그가 숨어 있는가?”라는 해석에도 이견이 생겨날 것입니다.
그렇다해도 프로젝트에서 개발할 때는 가능한한 공통의 인식을 지닐 것입니다.
거기서 이하의 항목에 대한 지식의 설명을 하고 싶습니다.(알고있는 항목이라면 넘어가주세요.)
- 커버리지율
- 동치분할과 경계값분석
- 매트릭스 해석
커버리지율이란 경로의 조합을 세는 방법입니다.
동치분할과 경계값분석이란 값의 패턴을 추출하는 방법입니다.
매트릭스 해석이란 이런저런 코드에 버그가 숨어있는지를 정량적으로 측정하는 방법입니다.
이것을 기억하는 것으로 어느정도 프로젝트간에 조합선택의 불균형이나 우선순위의 불규칙성이 없어질 것입니다.
커버리지율이란?
프로그램 내의 분기를 어느정도 망라했나를 나타내는 방법입니다.
커브리지율에는 C0, C1, C2라는 레벨이 있습니다.
이 코드의 예로 설명하겠습니다.
def my_method
if type1 == "A"
print("처리1")
else
print("처리2")
end
if type == "B"
print("처리3")
end
end
C0 : 명령망라
C0레벨에서는 명령을 망라해있는지 어떤지를 판단합니다.
즉 처리1 ~ 처리3을 한 번씩 지나가면 문제 없습니다.
def my_method
if type1 == "A"
print("처리1")
else
print("처리2")
end
if type == "B"
print("처리3")
end
end
테스트 케이스는 이하의 두 개로 C0레벨의 커버리지율이 100%라고 할 수 있습니다.
type == "A" TRUE, type == "B" TRUE
type == "A" FALSE, type == "B" TRUE
C1 : 분기망라
C1레벨에서는 분기를 망라해있는지 어떤지를 판단합니다.
즉 모든 분기를 한번씩 통과하면 오케이입니다.
def my_method
if type1 == "A"
print("처리1")
else
print("처리2")
end
if type == "B"
print("처리3")
end
end
테스트 케이스는 이하의 4가지로 C1레벨의 커버리지율이 100%라고 말할 수 있습니다.
2 + 2 = 4가지
type == "A" TRUE, type == "B" TRUE
type == "A" FALSE, type == "B" TRUE
type == "A" TRUE, type == "B" FALSE
type == "A" FALSE, type == "B" FALSE
C2: 조건망라
C2레벨에서는 분기의 조합도 전부 망라했는지 아닌지 판단합니다.
이것은 이상적인 테스트에 꽤 가까워서 큰일입니다.
def my_method
if type1 == "A"
print("처리1")
else
print("처리2")
end
if type == "B"
print("처리3")
end
end
테스트 케이스는 이하의 4가지입니다만, 실제로는 조건이 늘어나면 지수함수적으로 테스트케이스가 늘어 납니다.
2 * 2 = 4가지
type == "A" TRUE && type == "B" TRUE
type == "A" FALSE && type == "B" TRUE
type == "A" TRUE && type == "B" FALSE
type == "A" FALSE && type == "B" FALSE
동치분할과 경계값분석
그럼 여기까지 설명으로 조건분기의 수로 조합이 카운트되는 것이 이해되었습니다.
하지만, 아직 하나 더 조합의 변수가 있습니다. 그것은 값을 취할 수 있는 범위입니다.(치역)
케이스 스터디
이하의 코드의 테스트전략에 대하여 생각해보겠습니다.
# var이 꺼내오는 값은 String, Int, nil의 어느것인가?
def my_method(var)
var.your_method
end
커버리지적으로는 한 번으로 충분합니다
하지만, 커버리지율만 생각하는것으론 불충분합니다.
이번이라면 String, Int, nil의 케이스를 최저한으로 테스트하고 싶을 것입니다.
커버리지가 경로의 망라라면 이번에는 값의 망라입니다.
하지만, 값의 망라는 무한의 가지수가 있습니다 (자연수만 해도..)
그러면, 어떻게 망라하면 좋을까요??
동치분할
예와 같이 해설은 다른 이해하기 쉬운 기사에 맏기겠습니다.
동치분할이란 뭘까?(일본어)
경계값분할
그 이름 그대로 경계값을 테스트하는 것입니다.
매트릭스 해석이란
소프트웨어를 정적으로 분석하여 여러가지 관점으로부터 품질을 정량평가하는 것입니다.
이 사고방식을 아는 것으로 어떤 소스코드에 버그가 많은가? 라고 하는 것을 어느 정도 알 수 있습니다. 자세한 설명은 이하에 참고링크를 적어두겠습니다만 순환복잡도라고 불리우는 지표만 설명하겠습니다.
순환복잡도란?
소스코드 경로의 수를 세는 것으로 복잡도를 표현하는 방법입니다.
그렇다고 하더라도, 여기의 기사가 압도적으로 알기 쉽게 되있으므로 꼭 봐주세요.
\버그다-!/ 시스템에 서식하는 버그님으로부터 본 퀘적함, 순환복잡도(Cyclomatic Complexity)란? \버그다-!/ (일본어)
참고링크
매트릭스 해석이란?
Ruby on Rails | metric_fu로 매트릭스 해석 (일본어)
정적코드해석툴인 MetricFu는 무엇을 보고 있는 것인가? (일본어)
각언어 별 매트릭스해석 툴
처음하는 소프트웨어 매트릭스(전편) : 소프트웨어의 품질을 수치화하여 확인한다 (일본어)
테스트 케이스를 선택하는 방법을 고르고 싶을 때 읽는다.
「지식제로로부터 배우는 소프트웨어 아티스트」를 읽고 「화이트박스 테스트부터 탐색적 테스트까지」 – (일본어)
Q&A
거의 다 작성했습니다. 하지만, 프로젝트멤버 전원이 테스트의 품질을 지키는 것은 어렵습니다.
여기서, 지금까지 나온 논점 등을 Q&A방식으로 작성해보도록 하겠습니다.
질문1 : 유닛테스트와 E2E테스트는 어떻게 가려 써야 하나요?
이상적으로는 모두 함수, 메소드의 행동거지에 대하여 유닛테스트하여
그것들의 모든 조합을 E2E테스트로 테스트하는 것입니다.
하지만 현실적으로 그것은 어렵습니다.
그래서 복잡도가 낮은 함수, 메소드는 유닛테스트를 하지 않습니다.
독립해있는 조합은 테스트 케이스로부터 솎아냅니다.
이런 사고방식으로 테스트를 설계합니다.
번역 후기
이번이 세 번째 번역인데 꽤 장문의 글이였습니다. 후반부는 다른글로 링크가 많아서 자세히 이해하기 위해서 부족한 부분도 있겠지만 앞 부분에서 테스트를 어떤 생각을 해서 작성해내는지에 대한 부분만으로도 꽤 좋은 내용이 많기 때문에 문제가 없을 것이라 생각합니다. 같은 한자 문화권이고 일본의 영향을 많이 받아 해석할 때 기계적으로 하게 될 줄 알았는데 의외로 한자 단어들이 한국이랑 다르게 쓰이는 경우가 있었어서 재미있게 번역한 글인 것 같습니다.