Testable Design Fundamentals
In other to make software testable, designer should follow some rules. If you are interested in software design, some of them must be familiar to you. That’s because they are rules for improving software ‘design‘ in testability perspective. Lets look into the rules one by one.
- Clear Specification: Specification should cover all possible situations even for illegal conditions. Moreover, it must be clearly defined without any ambiguous sentences.
- Controlability: Target should provide mechanisms to read/write the conditions or to run the operations which are required to verify its functionality. It should be easy enough to implement.
- Modularity: Adequate modularization is one of the fundamental requirements not only for design, implementation, and reuse, but also for test. For an example, a module which has many relationships with other modules is hard to test independently. It should use stub/mock object or wait until the dependant modules are ready.
- Readability: Easy and intuitive naming reduces human errors and decreases design/implementation time. It makes overall testing time shorter.
- Consistency: All of the above rules should be reflected consistently so that the software can be look like designed and implemented by one person.
Software which satisfies these rules can be called testable. However, it is very hard to measure quantitatively how well the rules are reflected. I’ll remain this issue for other dedicated articles.
(in Korean)
테스트 가능한 소프트웨어를 만들기 위해서는, 설계자는 몇 가지 규칙을 따라야 한다. 소프트웨어 설계에 관심있는 사람이라면, 친숙한 이름의 규칙들을 찾을 수 있을 것이다. 이유인 즉, 이 규칙들은 테스트의 관점에서 소프트웨어의 ‘설계‘를 향상시키기 위한 지침이기 때문이다. 그럼 그 규칙들을 하나씩 살펴보도록 하자.
- 명확한 기능 명세: 소프트웨어 명세서는 잘못된 상황까지 포함한 모든 가능한 상황을 기술해야 한다. 이 때, 의미가 분명한 문장만을 사용해야 한다.
- 조작성: 테스트 대상은 그 기능 동작 여부를 판단할 수 있는 정보를 읽고/쓰거나 기능을 동작시킬 수 있는 메커니즘을 제공해야 한다. 또한 소프트웨어 적으로 쉽게 구현할 수 있어야 한다.
- 모듈화: 적절한 모듈화는 소프트웨어 설계, 구현, 재활용 뿐 아니라 테스트를 위해서도 꼭 필요하다. 예를 들어, 다수의 다른 모듈과 종속성이 있는 모듈은 독립적으로 테스트하기 어렵다. Stub/Mock Object를 사용하거나, 타 모듈들이 구현되기까지 기다려야 한다.
- 가독성: 쉽고 직관적인 이름(모듈, 함수 등 모든 경우에 해당됨)은 휴먼 에러(human error)를 줄여주고, 설계와 구현 시간을 단축시킨다. 결과적으로 테스트에 소요되는 전체 시간을 단축시키는 효과가 있다.
- 일관성: 이상의 모든 규칙들은 마치 한 사람의 설계하고 구현한 것처럼 일관성 있게 적용되어야 한다. 각각의 모듈마다 그 정도가 다르다면 휴먼 에러(human error)를 증가시킬 것이다.
이상의 규칙들이 충족된 소프트웨어라면 ‘테스트할 수 있다’고 얘기할 수 있을 것이다. 단, 이런 규칙들이 얼마나 잘 반영되었는지를 정량적으로 측정하는데는 분명한 한계가 있다. 측정 이슈는 주제의 범위를 벗어나므로 본 글에서는 더 자세히 다루지 않을 것이다.
TDF/TDAP Relationship
|
Clear Spec |
Controlability |
Modularity |
Readability |
Consistency |
Believe Me |
|
X |
|
|
|
Ambiguous Bounds |
X |
|
|
|
|
Ambiguous Activity |
X |
|
|
|
|
Take My Heart |
X |
|
|
|
|
Catch Me, If You Can |
|
X |
|
|
|
All for One |
|
X |
X |
|
|
Not Mandatory |
X |
X |
|
|
|
Don’t Call Me, I’ll Call You |
X |
X |
|
|
|
Unrecoverable Side-effect |
|
X |
|
|
|
Read Only |
|
X |
|
|
|
Case by Case |
|
|
|
X |
X |
High Coupling |
X |
|
X |
|
|
Tacit Agreement |
X |
|
|
|
|
Testable Design Anti-Patterns
Believe Me
기능을 수행시킬 수는 있으나, 수행 결과를 확인할 수 있는 방법이 제공되지 않는다.
리턴 타입이 void 이고 관련된 get 함수도 전혀 제공되지 않는 경우가 대표적이다. 최소한 성공/실패 여부라도 알 수 있어야 하나 접근할 수 있는 어떠한 수단도 없다.
하위 플랫폼이 상위 어플리케이션에게 접근을 불허하는 작업을 수행하는 경우도 마찬가지 이다. 예를 들어 C언어에서 malloc으로 할당 받은 메모리를 free했을 때 정말로 free가 되었는지, 또는 Java에서 garbage가 GC에 의해 수집되었는지 등은 상위 언어 수준에서는 검증할 수 없다.
참고로, 클래스 생성자의 경우 코드 상으로는 출력 타입이 없더라고, 호출 결과로 객체가 생성되어 반환되므로 Believe Me 패턴에 해당하지 않는다.
Effects On Test: 기능 수행 후 시스템이 다운되거나 정지하지 않는지 여부만 판단할 수 있다.
How to Improve: 동작 성공 여부, 에러 발생 여부 (+ 어떤 에러인지)를 확인할 수 있는 메커니즘을 함께 제공한다. 다음과 같은 방법들이 있을 수 있다.
- Return 타입 활용 (잘 정의된 RESULT 타입, 또는 수행 결과를 직접적으로 알려주는 값)
- Exception 발생
- 상태 변화를 알 수 있는 별도의 get 메서드 제공.
Related Patterns: Catch Me, If You Can. / Don’t Call Me, I’ll Call You.
Ambiguous Bounds
입력값의 유효 범위를 명확히 기술하지 않는다.
Null 포인터, 0, 음수값이 유효한 지 여부를 항상 신경써야 하며, 하나의 enum 타입을 여러 함수에서 사용할 때, 같은 값이라도 함수마다 valid/invalid 여부가 달라지는 경우도 있으니 주의해야 한다.
스펙이 명확하지 않을 때는 종종 개발자가 스스로 자신의 상식에 근거해 구현하는 현상이 목격되는데, 이는 개발자마다 경험과 사고체계가 다르므로 잘못 사용될 소지가 다분하다.
실생활에선 경계값이 사용되는 빈도는 상대적으로 적기 때문에 경계값에 의해 발생하는 이상동작은 쉽게 파악하지 못할 수도 있다.
Effects On Test: Boundary Value를 정하지 못하는 문제가 발생한다.
개발자가 그러하듯, 숙련된 테스터가 아니라면 경계값을 스스로의 상식에 근거해 규정하기도 한다. 이 경우 우연히 개발자의 구현과 일치하면 테스트가 Pass로 처리되나, 실제 사용하면서 뒤늦게 문제가 발견되곤 한다. 개발자와 테스터가 동일 인물일 경우 그 가능성은 거의 100%에 가깝다.
숙련된 테스터라 하더라도, 모호한 경계값에 대해 개발팀에 문의/확인하는 오버헤드는 피할 수 없다.
How to Improve: API 설계 시, 항상 valid/invalid 영역이 명확히 구분되는지 확인하고, 스펙에 명시하여야 한다.
테스트 케이스 작성시 경계값을 사용하는 습관을 들이면 이 문제를 예방하는데 큰 도움이 된다.
경계값을 구분에서 그치지 않고, invalid 값이 들어왔을 때 처리 방식도 함께 명시되어야 한다.
Review Checklist에 이 항목을 추가하는 것도 실수를 방지하는 좋은 실천 방법 중 하나이다.
Related Patterns:
Real World Examples:
#1. 아래와 같이 한 쌍의 request/response 함수가 있다. 근처 아바타들을 찾으라는 request 함수와, 그 결과를 반환하는 response함수로 구성된다. Response 함수는 찾은 아바타의 수와 그 수 만큼의 아바타 정보를 포인터 배열로 넘겨준다. 그리고 이름은 Java의 리스너 메서드에서 많이 쓰이는 패턴을 따왔다.
void findNearbyAvatars(float radius); // asynchronous request.
void avatarFound(unsigned short count, const Avatar* pAvatars); // response callback. |
여기서 문제가 되는 부분은 어디일까? 바로 response 함수의 이름과 count의 관계이다. 질문을 던져보기로 하겠다. count는 0이 될 수 있는가? count 값이 0인 것은 예외적인 상황인가 정상적인 상황인가? 우리는 이를 다음과 같은 두 가지 관점에서 바라볼 수 있을 것이다.
- 기능 관점: avatarFound는 ‘근처 아바타를 찾아라’는 명령의 응답이며, 따라서 근처에 아바타가 없다면 당연히 count는 0이 될 것이다. 이는 극히 정상적인 상황으로 절대 예외가 아니다.
- 네이밍 관점: avatarFound는 무언가 찾았다는 의미이다. 따라서 아무것도 찾지 못했다를 의미하는 ‘0′ 은 예외적인 상황이다. 따라서 예외를 알려주는 별도의 인자나 avatarNotFound 와 같은 추가적인 인터페이스가 필요할 것이다.
이처럼 바라보는 개발자의 관점에 따라 count 의 유효 범위가 달라질 소지가 농후하기 때문에 스펙상에 명확한 기술이 있어야 한다. 물론 더 나은 방법은 헷갈리는 이름을 사용하지 않는 것이다. 취향에 따라 좋아하지 않을 사람도 있긴 하겠지만, ‘requestNearbyAvatars/responseNearbyAvatars’ 와 같은 이름이라면 스펙을 자세히 읽지 않더라도 대부분의 개발자가 ‘count >= 0′ 으로 인지하는데 무리가 없을 것이다.
Ambiguous Activity
특정 상태에서 함수가 어떻게 동작하는지 확신이 서지 않는다.
상태 머신으로 표현될 수 있는 시스템에서, 각 상태에 따른 복잡한 상황을 미처 다 고려하지 못해 주로 발생한다. 스펙 작성자가 능숙하지 않다면 상태 머신인지조차 인식하지 못하는 경우도 많다.
다음은 흔히 볼 수 있는 Ambiguous Activity 사례들이다.
- 이미 저장되어 있는 객체를 다시 저장하려 할 때. (무시? 중복 저장? or 예외 발생?)
- 저장되어 있지 않은 원소를 제거하려 할 때.
- Max capacity를 초과해 저장하려 할 때.
- 이미 close 된 스트림을 다시 close 하려 할 때.
- 이미 초기화된 객체를 다시 초기화하려 할 때.
- 초기화되지 않은 객체를 사용하려 할 때.
Effects On Test: 가장 문제가 되는 경우는 스펙에 아무런 언급이 없어, 테스터도 이를 미쳐 발견하지 못하고 지나칠 때이다. 이렇게 되면 필요한 테스트를 다 하지 못하고 제품을 출시하게 된다. 다행히 테스트 커버리지 측정이 잘 이루어지고 있다면 뒤늦게라도 발견하고 개선할 여지가 있다.
문제가 발견될 경우, 복잡한 상황에 대한 세밀한 규정 작업이 요구되므로 상대적으로 많은 시간 지연이 발생될 수 있다.
How to Improve: 스펙 차원에서 각 상태에서의 기능을 명확히 기술해야 한다.
모든 가능한 State를 추출한 후, 각 State에서의 제약 사항을 기술한다.
UML State Diagram 등을 이용하면 도움이 될 것이다.
Related Patterns: Tacit Agreement
Take My Heart
SUT의 내부 데이터의 조작권을 통체로 내어준다.
예를 들어 배열, 리스트 등 collection의 포인터를 통체로 내어주는 get 함수가 있을 경우, 반환된 collection을 외부 모듈에서 변경(add/remove/set 등)하거나 free 하는 명령이 SUT의 내부 상태에 직접 영향을 미친다. 혹 이러한 사항을 스펙에 명시하더라도 사용자가 명확히 인식하지 못하고 사용할 경우 SUT의 안정성에 큰 악영향을 미칠수 있다.
Effects On Test: 앞서 수행된 테스트 케이스에 의해 side-effect가 발생하기 쉽다. Side-effect를 없애기 위한 상태 복원(tear-down) 작업이 쉽지 않다.
How to Improve: 시스템적으로 신뢰할 수 있는 경우를 제외하고, 주요 데이터를 내어줄 때는 항상 복사본을 만들어 넘긴다. 부득이한 경우 사용자가 위험성을 최대한 인지할 수 있도록 스펙에 분명히 명시한다.
기초적인 실수는 audit (정적 분석) 툴에서 지적해주고 있으니 이들을 적극 활용하는 것도 좋은 예방법이다.
Related Patterns: Unrecoverable Side-effect
Real World Examples:
#1. 아래의 Group 클래스는 한 그룹의 멤버 정보를 담고 있는 저장소이고, getMembers 함수를 통해 등록된 멤버들의 리스트 알 수 있게 해준다.
public class Group {
List members = new List();
…
public List getMembers() {
return members;
}
} |
쉽게 파악할 수 있듯이, 멤버 정보 관리용으로 내부적으로 선언한 members 리스트의 레퍼런스를 직접 어플리케이션에 노출한 것이 문제이다. 이 경우 어플리케이션은 Group 이라는 공식적인 클래스를 거치지 않고 직접적으로 멤버를 추가/변경/삭제할 수 있다. 만약 Group 클래스가 특정 Admin 권한(group owner 등)의 멤버를 특별 관리 한다거나, 등록/추가에 동기화를 수행한다거나 하는 부수적인 로직이 들어가 있다면, 어플리케이션은 아무런 제약 없이 이를 피해갈 수 있다. 많은 경우, 어플리케이션 제작자들은 자신의 수정 사항이 시스템 내부의 데이터를 직접 변경할 것이라는 의식조차 없는 상태에서 이런 일들을 수행한다. 의식하지 못한다는 것은 문제 발생 시 원인 파악이 어렵다는 것과 동의어이다.
Catch Me, If You Can
Believe Me 패턴의 특수한 경우로, 수행 결과를 만들어는 내지만 TestCase가 소프트웨어 적으로 알아낼 수 없는 엉뚱한 곳으로 전송한다.
화면(display) 출력을 이용하거나, 임의의(temp or log) 파일을 이용하는 경우가 대표적이다.
참고로 이 패턴은 Case by Case 패턴과 함께 발견되기 쉽니다.
즉, 결과를 화면에 출력하면서, 출력 포맷이 일정하지 않아 혹 TestCase에서 스트림을 가로채더라도 파싱하여 결과를 판단하기 어렵게 만든다.
Effects On Test: TC 상에서 프로그램적으로 결과를 확인할 수 없다.
결과를 제대로 확인할 수 없다는 측면에서 Believe Me 패턴과 동일하다. 즉, 시스템의 정지/다운 여부만을 판단할 수 있다.
How to Improve: 꼭 필요한 경우가 아니라면 화면 출력이나 임의 파일의 사용을 피하고 TestCase에서 직접 확인할 수 있는 다른 수단을 제공한다. (Believe Me의 개선 방법 참조)
꼭 필요한 경우라면 출력 스트림을 TestCase에서 재설정하거나 가로챌 수 있는 메커니즘을 제공한다.
범용적인 TestCase를 제작해 재사용하고 싶다면 이상의 결과 확인 수단이 스펙 상에 반드시 명시되어 있어야 한다.
Related Patterns: Believe Me, Case by Case.
All for One
Compact한 여러 모듈로 나누어 구현할 수 있는 것을 하나의 커다란 모듈로 구현하여 복잡도가 지나치게 높아졌다.
- 다양한 조건들을 모두 입력 파라미터로 만들었다.
- #ifdef 류의 문장들이 한 함수 내에 복잡하게 설켜 있다.
일반적으로 복잡한 하나의 모듈을 테스트하는 것보다 단순한 여러 모듈을 테스트하는 것이 설계, 구현, 검증, 유지보수 등 모든 면에서 유리하다.
Effects On Test: 테스트 가능한 조합의 수가 너무 많아 테스트에서 누락되기 쉽다. 입력 값이나 상태에 따른 종속성을 명시하기 쉽지 않아 결함이 발생할 확률이 높다.
How to Improve: 보다 작은 여러 개의 모듈로 나누어 구현한다. 정적 분석 툴을 활용하여 복잡도(Cyclomatric complexity 등)가 지나치게 높은 모듈을 식별하여 개선한다.
Related Patterns: High Coupling
Not Mandatory
동작, 입력 등에 대한 명세는 확실하나, mandatory가 아니며, 지원 여부를 TestCase가 소프트웨어적으로 알 수 없는 경우이다.
크게 보면 상황에 따라 동작 방식이 달라지는 경우이나, 그 상황을 TC에서 제어할 수 없거나, 너무 힘든 경우가 될 수 있다. 컴파일 옵션에 따라 달라지는 상황도 하나의 예가 될 수 있다.
Effects On Test: 검증의 관점에서는 mandatory 가 아닌 상황은 그 대상에서 제외된다. 지원한다고 가정하고 테스트를 진행하였으나, 제대로 동작하지 않아도 실패로 처리할 수 없기 때문이다. 성공/실패 여부를 판단할 수 없으면서 기능을 수행시켜보는 것 역시 큰 의미가 못된다.
How to Improve: 각 기능의 지원 여부를 TestCase에서 소프트웨어적으로 알 수 있는 메커니즘을 지원한다. isXxxSupported 함수 등의 query 메서드를 제공하여 TestCase가 runtime에 테스트 정책을 판단할 수 있도록 한다. 이와 함께, 기능이 지원되지 않는 상태에서 API를 호출했을 때의 처리 방식도 명확히 기술되어야 한다. 예를 들어, 호출을 무시하고 아무런 동작도 하지 않는다던 지, 미리 정의된 예외를 발생시킬 수 있다.
Related Patterns:
Don’t Call Me, I’ll Call You
Callback 함수(listener)를 등록할 수는 있으나, callback(notify)되는 상황을 만들 수 없다. 또는 직관적으로 알기 어렵다.
Effects On Test: 결과를 확인할 방법이 없으므로, 단순히 API를 호출해보는 것 외에는 할 수 있는 일이 없다.
How to Improve: Callback 함수가 호출되는 상황을 명확히 기술한다.
TestCase가 소프트웨어적인 조작을 통해 임의로 호출 상황을 만들 수 있도록 한다.
Related Patterns: Believe Me
Etc: 이 패턴은 Ambiguous Activity 패턴과 함께 발견되는 사례가 많다. 다음은 그 예이다.
- 같은 listener를 중복 등록하려 할 때, 두 번째 요청의 처리 방식 – 무시? 예외 발생? 중복 허용?
- 등록되지 않은 listener를 제거할 때 처리 방식 – 무시? 예외 발생?
Real World Examples:
#1. 아래의 예가 발견된 시스템 전체를 이해하려면 추가적인 설명이 필요하다. 사용자가 서버에 로그인을 하면 사용자 계정의 인벤토리가 클라이언트에 자동으로 patch 되고(prepatch), 인벤토리의 몇몇 기능은 patch가 완료된 이후에야 서비스가 가능하다. 코드에 보이는 것과 같이 prepatch 의 완료 여부를 알 수 있는 두 가지 메커니즘이 제공된다.
public interface InventoryListener {
public …
public void prepatchCompleted(..)
}
public class Inventory {
public …
public bool isPrepatchCompleted() {..}
} |
어플리케이션 개발자 입장에서는 여러 가지가 궁금해질 것이다.
- prepatch는 정상적으로 시작이 되었는가?
- 문제 없이 진행되고 있는가?
- 그렇다면 언제쯤 완료될 것인가?
- 응답이 없다면 다시 시작하는 걸 요청할 수 있는가?
- prepatch 완료 여부에 종속적인 API들은 어떤 것들이 있고, 완료되지 않았을 경우 어떻게 반응하는가? 이들을 사용해도 무방한가?
사용자에게 ‘언젠가 끝날 터이니 맘 편히 완료될 때까지 기다리라’고 하는 것은 좋은 설계 태도가 아니다. 특히나 오랜 시간이 소요되거나, 소요 시간의 편차가 심할 경우에는 보다 세밀한 상태 확인 메커니즘을 제공해주는 것이 좋다.
Unrecoverable Side-effect
테스트 수행 과정에서 만들어진 side-effect를 원상태로 되돌릴 수 없다.
보통 너무 방어적으로 설계하는 경우 자주 볼 수 있는 패턴으로, Use-Case 상에 명확히 존재하는 API 만을 제공하여 테스트를 위해 필요한 조작 자체를 수행할 수 없게 만든다. 좀 더 세분화하면 side-effect가 영구적으로 남는 경우와, 복구되는 시점을 알 수 없는 경우가 있다. 다음은 그 실례들이다.
- 시스템 초기화 관련 API들의 경우, Runtime에서 시스템의 안정성을 확보하기 위해 의도적으로 재설정을 막는 설계가 많다.
- 하부 시스템에서 성능 향상을 위해 자동으로 자원을 pooling, caching하는 메커니즘을 제공할 때, 확보된 자원을 상위 어플리케이션에서 해제하는 API를 제공하지 않는다.
- Java Virtual Machine에서의 Garbage Collection, Native Resource (thread, socket, file) 관리 방식 등이 이에 해당한다.
Effects On Test: 각각의 TestCase는 정상 동작하나, 그 결과가 계속 잔존하여 다음 TestCase들이 원래 의도한 테스트를 정상적으로 수행하지 못한다. 개별 테스트와 자동화된 테스트에서 다른 결과를 산출하기 쉽다. 또한 숨겨진 side-effect를 찾아 분석하기 쉽지 않으므로 디버깅 난이도도 증가한다.
결과적으로 정확한 테스트를 위해서는, 각각의 TestCase가 시스템을 재시작하여 깨끗한 상태에서 수행시켜야 하나, 이는 구현 난이도와 작업량 증가, 테스트 소요 시간을 증가시킨다.
How to Improve: 가능한 모든 상태 변화를 외부에서 조작할 수 있도록 API를 공개한다.
시스템의 안정성, 일관성 등을 위해 보호해야 하는 기능이라면 보안 메커니즘을 통해 특정 권한이 있는 어플리케이션의 조작을 허용하거나, 별도로 모든 기능을 사용할 수 있는 테스트 모드 개념을 도입한다.
다른 이유에 의해 조작을 절대 허용할 수 없다면, 상태 정보를 최대한 공개하여 TestCase가 스스로 상황을 판단해 테스트를 조절할 수 있도록 한다. 이 경우, 각각의 TestCase들은 runtime 정보를 바탕으로 테스트 데이터를 다르게 사용하거나, 테스트 자체를 skip 할 수 있다.
Related Patterns: Take My Heart
Read-Only
런타임에 변경 가능성이 있는 값들에 대해 Set 하는 방식이 명시되지 않은 채(또는 소프트웨어적인 조작 메커니즘을 제공하지 않고) Get 함수만 제공된다.
Set을 할 수 없는 상황이므로, Get 된 결과가 상황에 적절한 값인지를 확인하기 어렵다.
참고로 제품의 제작사 정보, 버전 넘버 등 런타임에 변경 가능성이 없는 정적 데이터들의 경우는 이 패턴의 대상이 아니다.
Effects On Test: TestCase가 결과는 알 수 있지만, 결과에 어떠한 의도를 반영한 것이 아니므로 별다른 의미를 부여할 수 없다. TestCase에서 판단할 수 있는 것은 결과가 정해진 valid 범위 내의 값인가를 판단하는 것뿐이다. 예를 들어 리턴값이 문자열일 경우 null 이나 empty string 이 아닌지, 남아 잇는 자원의 개수일 경우 음수가 아닌지 등을 검사할 수 있다. 물론 valid/invalid 범위가 스펙에 명시되어 있어야 한다.
How to Improve: 직접적인 Set 함수가 아니더라고 Get 함수의 결과값을 의도대로 변경시킬 수 있는 API를 제공한다. 이와 함께 Get 함수가 반환하는 값의 유효 범위를 명시한다.
Related Patterns:
Case by Case
하나 하나의 모듈만 보면 문제가 없으나, 전체 시스템 관점에서 보면 각각의 모듈에 적용된 규칙에 통일성이 결여되어 있다.
예를 들어 입력/예외 처리, 결과 확인 방법, 모듈화 수준, 명명 규칙 등이 모듈마다 차이가 있을 수 있다. 설계/코딩 규약이 없거나 강제하지 않을 때, 리뷰 프로세스가 잘 이뤄지지 않을 때 자주 발생한다.
Effects On Test: 테스터가 테스트 대상에 익숙해질 수록 문제가 발생하기 쉽다.
How to Improve: 설계/코딩 규약을 명확히 하고, 리뷰를 통해 잘못된 부분들을 바로잡는다. 정적 분석 툴을 이용하여 잘못된 네이밍 등을 조기에 처리한다.
Related Patterns:
High Coupling
수행 결과가 너무 많은 곳에 영향을 미친다. 또는 반대로 너무 많은 곳으로부터 영향을 받는다.
모듈간의 low coupling이라는 기본 설계 지침이 잘 지켜지지 않았을 때 발생하며, 보통 이런 경우는 그 파급 효과가 스펙에서 누락되기 쉽다.
Effects On Test: pre/post-condition을 확인하기 어려워 잘못된(또는 불완전한) 테스트 케이스가 만들어질 우려가 있다. 테스트 케이스의 품질과 상관 없이, 테스트된 모듈에 대해서는 일단 믿는 경향이 강하므로 엉뚱한 부분에서 에러 원인을 찾게 된다.
How to Improve: 모듈간 적절한 역할 분담을 통해 종속성을 최대한 줄일 수 있도록 재설계한다. 영향을 주고 받는 부분을 스펙에 명시한다.
Related Patterns: All for One
Tacit Agreement
스펙에 명시되지 않은 특정한 상황(시스템 상태, 입력값)이 항상 만족함을 가정하고 구현한다. 가정이 만족되지 못하면 시스템은 불안정한 상태에 빠질 수 있다.
개발 중심의 조직이나, 빡빡한 스케줄에 의해 설계와 테스트에 비중을 두기 어려운 상황에서 자주 발생하는 잘못된 패턴이다. 또한 테스트에 능숙하지 않은 개발자는 Unit Test Case를 만들 때, 설계가 아닌 구현을 기반으로 만드는 경향이 있다. 이 것도 Tacit Agreement 패턴의 일종이다.
Effects On Test: 스펙을 기준으로 만들어진 Test Case는 fail 될 확률이 높다. 테스트와 개발자 사이의 커뮤니케이션 비용이 증가도 문제이지만, 이런 일이 자주 발생되면 자칫 테스터는 스펙이 아닌 구현 코드를 기반으로 Test Case 만들 수 있다. 구현이 잘못되어 있으면 잘못 구현된 대로 한 번 돌려보는 결과만 낳게 되는 심각한 후유증이 생긴다.
How to Improve: 구현 시 스펙에 명시되지 않은 가정을 피하고, 어쩔 수 없는 경우 해당 조건을 스펙에 명시한다.
Related Patterns: Ambiguous Activity