(Working Effectively with Legacy Code) 011. I Need to Make a Change. What Methods Should I Test?

I Need to Make a Change. What Methods Should I Test?

코드를 변경하기 전, 기존 동작을 좀 더 명확하게 정의하기 위해 문서화 테스트(characterization test) 를 작성해야한다고 하자.

문서화 테스트는 어디에 작성해야할까?

변경 대상인 메서드마다 테스트 코드를 작성하는 것으로 충분할까?

코드의 동작이 간단하고 이해하기 쉽다면 충분하겠지만, 레거시 코드라면 이야기가 달라진다.

레거시 코드 특성상 하나의 메서드가 여러 책임을 지고 있을 수도 있고, 무엇보다 변경시 다른 위치의 코드에 동작이 변경되었을 때

이를 감지하는 것이 쉽지않기 때문에 테스트 루틴의 위치를 선정하는 것이 매우 중요하고 할 수 있다.

따라서 복잡도가 높은 레거시 코드를 변경할 때에는 테스트 코드의 위치를 선정하는 데 충분한 시간을 들이는 것이 좋다.

어떤 변경을 수행하려는 지, 이 변경이 어떤 영향을 미칠지 그리고 영향받은 것이 또 다른 곳에 영향을 미치는 지 등을 면밀하게 검토해야하는 것이다.

추가로 복잡한 코드를 좀 더 나은 설계로 변경하는 리팩토링의 중요성은 개발자라면 익히 알고 있겠지만, 이 리팩토링의 전제조건이 테스트 코드로 보호되는 것임을 상기해보자.

테스트 코드가 존재하지않는 레거시 코드를 리팩토링하려면 어떤 위치에 테스트 코드를 작성해야할까?

이번 포스팅에서는 테스트 코드의 최적 위치를 찾기위한 기법들을 소개한다.

1. 영향 추론(Reasoning About Effects)

소프트웨어 내의 기능적 변경은 어떤 식으로든 연쇄적인 변경을 야기한다.

아래 예제코드를 보자.

1
2
3
4
5
6
7
8
int getBalancePoint() {
const int SCALE_FACTOR = 3;
int result = startingLoad + (LOAD_FACTOR * residual * SCALE_FACTOR);
foreach(Load load in loads) {
result += load.getPointWeight() * SCALE_FACTOR;
}
return result;
}

만약 위의 SCALE_FACTOR 상수의 값을 4로 바꾼다면 반환값이 달라지게되어, getBalancePoint() 메서드를 호출하는 모든 코드에 영향을 미치게 될 것이다.

마치 하나만 바꿔도 큰 영향이 생길 것같이 설명했지만 실제로는 그렇지않다.

애플리케이션의 모든 부분에서 특정 메서드를 접근하는 케이스는 거의 찾기 힘들기 때문이다.

중요한 것은 어떤 변경이 발생했을 때, 어디까지 영향을 미칠 것인가를 파악하는 것 이며 이 작업을 영향 추론(Reasoning About Effects) 이라한다.

이제부터 예제를 통해 영향 추론에 대해서 알아보도록 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class CppClass { 
private String name;
private List declarations;

public CppClass(String name, List declarations) {
this.name = name;
this.declarations = declarations;
}

public int getDeclarationCount() {
return declarations.size();
}

public String getName() {
return name;
}

public Declaration getDeclaration(int index) {
return ((Declaration)declarations.get(index));
}

public String getInterface(String interfaceName, int [] indices) {
String result = "class " + interfaceName + " {\npublic:\n";
for (int n = 0; n < indices.length; n++) {
Declaration virtualFunction = (Declaration)(declarations.get(indices[n]));
result += "\t" + virtualFunction.asAbstract() + "\n";
}
result += "};\n";
return result;
}
}

위의 CppClass 클래스는 C++ 소스 코드를 불러와 해석하는 프로그램이다.

CppClass 클래스의 객체를 생성한 후 수행가능한 변경 중에서 객채 네 메서드의 반환값에 영향을 미치는 항목은 아래와 같다.

  1. 생성자에 전달되는 declarations 리스트를 전달한 뒤, 이 리스트에 새로운 값이 추가될 수 있다. 값이 변경되면 getDeclarationCount(), getDeclaration(), getInterface() 메서드의 결과값이 모두 변경된다.

  2. declarations 리스트에 값이 추가되지않더라도, 기존 값이 변경된다면 영향을 미칠 수 있다.

이를 다이어그램과 함께 좀 더 명세해보자.

getDeclarationCount()에 미치는 영향

Figure 11.1 declarations impacts getDeclarationCount.

declarations 리스트가 변경되면 getDeclarationCount() 메서드의 반환값이 변경된다.

getDeclaration()에 미치는 영향

Figure 11.2 declarations and the objects it holds impact getDeclarationCount

declarations 리스트가 변경되거나, 기존 원소의 값이 변경되면 getDeclaration() 메서드의 반환값이 변경될 수 있다.

getInterface()에 미치는 영향

Figure 11.3 Things that affect getInterface.

declarations 리스트가 변경되거나, 기존 원소의 값이 변경되면 getInterface() 메서드의 반환값이 변경될 수 있다.

모든 다이어그램을 한 번에 도식하면 아래와 같다.

Figure 11.4 Combined effect sketch.

만약 코드가 좋은 설계를 기반으로 구조화가 잘 되어있을 수록, 좀 더 간단한 다이어그램이 도식될 것이다.

이번엔 CppClass 클래스를 포함하는 ClassReader라는 클래스를 통해 좀 더 넓은 범위의 영향을 추론해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ClassReader {
private boolean inPublicSection = false;
private CppClass parsedClass;
private List declarations = new ArrayList();
private Reader reader;

public ClassReader(Reader reader) {
this.reader = reader;
}

public void parse() throws Exception {
TokenReader source = new TokenReader(reader);
Token classToken = source.readToken();
Token className = source.readToken();

Token lbrace = source.readToken();
matchBody(source);
Token rbrace = source.readToken();

Token semicolon = source.readToken();

if (classToken.getType() == Token.CLASS
&& className.getType() == Token.IDENT
&& lbrace.getType() == Token.LBRACE
&& rbrace.getType() == Token.RBRACE
&& semicolon.getType() == Token.SEMIC) {
parsedClass = new CppClass(className.getText(), declarations); // HERE
}
}
// ...
}

우리는 CppClass 클래스에서 추론한 영향 범위를 통해 생성자를 통해 넘어오는 declarations 리스트값의 변경 여부가 중요함을 알고 있다.

다행히 위의 코드에서 CppClass 객체를 생성하고 declarations 리스트를 주입하는 곳은 단 한 곳임을 알 수 있다.

1
parsedClass = new CppClass(className.getText(), declarations); // HERE

이번엔 declarations 리스트에 항목이 추가되는 코드를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void matchVirtualDeclaration(TokenReader source) throws IOException {
if (!source.peekToken().getType() == Token.VIRTUAL) {
return;
}

List declarationTokens = new ArrayList();
declarationTokens.add(source.readToken());

while(source.peekToken().getType() != Token.SEMIC) {
declarationTokens.add(source.readToken());
}

declarationTokens.add(source.readToken());
if (inPublicSection) {
declarations.add(new Declaration(declarationTokens)); // HERE
}
}

matchVirtualDeclaration() 메서드를 호출하면 source로부터 값을 읽어와, declarations 리스트에 항목을 추가하고 있음을 확인할 수 있다.

여기서 파악되는 점은 declarations 리스트에 항목을 추가되는 Declaration 객체는 생성된 후 별도로 변경이 없다는 점이며,

이를 통해 CppClass 클래스의 객체가 생성된 뒤에는 declarations 리스트가 변경되지않았음을 추론할 수 있다.

지금까지 파악한 것을 바탕으로 결론을 내려보자.

ClassReader 클래스에서 생성되는 CppClass 객체는, 생성 이후에 별도의 변경 포인트가 존재하지않으므로,

CppClass 클래스가 의도하지않은 값을 가지고 있을 때, CppClass 클래스의 하위 객체부터 조사하면 되며, CppClass 클래스내의 상수에 대한 참조를 final로 변경하여 코드를 보다 안전하게 처리할 수 있다.

이처럼 다소 품질이 낮은 프로그램에서는 결과값이 도출될때까지의 과정을 파악하기 매우 힘들 때가 많다.

문제를 해결하기위해 결과로부터 원인까지 역으로 추적하는 과정에서도 계속해서 영향도를 파악해나가는 노력또한 필요한 것이다.

“어떤 변경의 수행이 나머지 부분에 어떤 영향을 줄 것인가?” 에 대한 답을 찾으려면 변경 지점을 기준으로 전방 추론(reasoning forward) 과정을 수행해야 한다.

전방 추론의 반복적인 연습을 통해 얻은 숙련도가 곧 테스트 루틴 작성 위치를 찾는 기초가 될 것이다.

2. 전방 추론(Reasoning Forward)

영향 추론에서는 코드의 특정 위치에 있는 값에 대해 어떤 객체들이 영향을 미치는 지에 대해 추론하였다.

반대로 문서화 테스트를 작성할 때는 먼저 객체들을 조사하고 객체들이 제대로 동작하지 않을 경우 어떤 영향을 미치는 지에 대해서 추론한다.

이번에도 예제를 통해 파악해보자.

아래 예제는 인메모리 파일 시스템의 일부로, 테스트 루틴이 존재하지 않는 코드이다.

이때 이 클래스에 대해 변경을 수행해야한다고 가정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class InMemoryDirectory {
private List elements = new ArrayList();

public void addElement(Element newElement) {
elements.add(newElement);
}
public void generateIndex() {
Element index = new Element("index");
for (Iterator it = elements.iterator(); it.hasNext(); ) {
Element current = (Element)it.next();
index.addText(current.getName() + "\n");
}
addElement(index);
}
public int getElementCount() {
return elements.size();
}
public Element getElement(String name) {
for (Iterator it = elements.iterator(); it.hasNext(); ) {
Element current = (Element)it.next();
if (current.getName().equals(name)) {
return current;
}
}
return null;
}
}

InMemoryDirectory 클래스를 명세하면 아래와 같다.

InMemoryDirectory 클래스는 Java로 작성되어 있으며 객체를 생성하여 Element 객체를 추가하고 인덱싱하여 접근할 수 있다.

여기서 Element는 파일처럼 텍스트를 포함하고 있는 객체이며, generateIndex() 메서드로 인덱싱을 수행할때는 index 라는 이름의 Element 객체를 생성하고 index의 텍스트에 다른 모든 요소들의 이름을 추가한다.

이 클래스의 특이한 점은 generateIndex() 메서드를 두 번 호출하면 두 개의 인덱스가 생기며, 나중에 생성된 인덱스는 처음 생성된 인덱스를 요소로서 포함하게 된다.

사실 InMemoryDirectory 클래스는 요소의 추가와 동시에 인덱스를 생성하고 유지보수하는 것이 가장 이상적이다.

이를 위해 새로운 동작을 추가하고 이에 대한 테스트 루틴을 작성하는 것은 간단하지만, 테스트 루틴이 존재하지않는 현재 동작에 대한 처리가 문제다.

기존 InMemoryDirectory 클래스에서 테스트가 필요한 부분은 addElement() 메서드를 호출하고 인덱스를 생성하며 요소 들이 제대로 저장되었는지 확인하는 부분이다.

현재 동작의 테스트 루틴은 어디에 작성해야할까?

가장 먼저 해야할 일은 변경해야할 위치를 판단하는 것이다.

먼저 generateIndex() 메서드에서 기능을 제거한 뒤 addElement() 메서드에 추가한 다음 다이어 그램을 그려보도록 하자.

generateIndex() 메서드를 호출하는 곳은 어디일까?

InMemoryDirectory 클래스내의 어느 곳에서도 호출하지 않는 것을 보아 외부 클래스에서 호출하고 있음을 추론할 수 있다.

다음으로 generateIndex() 메서드가 변경하는 대상은 무엇일까?

신규 요소를 생성하고 디렉토리에 추가하는 것이 generateIndex() 메서드의 동작이므로, element 변수에 영향을 줌을 알 수 있다.

Figure 11.5 generateIndex affects elements.

이제 포커스는 element 변수로 옮겨진다.

element 변수는 어디에서 사용될까?

InMemoryDirectory 클래스의 getElementCount() 메서드와 getElement() 메서드, addElement() 메서드에서 사용되고 있다.

이 중 addElement() 메서드는 element 변수의 상태와 관계없이 동작하므로 addElement() 메서드엔 영향이 없다고 할 수 있다.

Figure 11.6 Further effects of changes in generateIndex.

여기까지 변경 지점을 getElementCount() 메서드와 addElement() 메서드로 좁히고 나면, 이 두 메서드가 어떤 영향을 미치는 지도 파악해야한다.

addElement() 메서드는 element 변수에 요소를 추가하므로 영향을 미친다고 볼 수 있다.

Figure 11.7 addElement affects elements.

결론적으로 InMemoryDirectory 클래스가 어떠한 변경이 있었고, 이 영향을 감지하려면 getElementCount() 메서드와 getElement() 메서드를 통해 확인해야 하므로, 이 두 개의 메서드에 대해 테스트 루틴을 작성해야함을 추론할 수 있다.

전체적인 영향 다이어그램을 그려보면 아래와 같다.

Figure 11.8 Effect sketch of the InMemoryDirectory class.

여기서 한 가지 더 체크해봐야할 게 있는데, 바로 변수의 접근제어자와 클래스의 상속 구조이다.

참고 좀 더 명세하면 클래스의 슈퍼클래스나 서브클래스가 있는지, 변수가 public, protected, package로 접근제어자가 지정되었는 지를 뜻한다.

이번 예제의 경우 element 변수가 private 이므로 해당 사항이 없다.

헌데 마지막 영향 다이어그램을 보면 Element 클래스가 빠져있다.

InMemoryDirectory 클래스에서 Element 객체를 인자로 요구하거나, 반환하므로 Element 클래스의 코드도 살펴보도록 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Element { 
private String name;
private String text = "";

public Element(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void addText(String newText) {
text += newText;
}
public String getText() {
return text;
}
}

코드를 확인했으니, generateIndex() 메서드 호출시 생성하는 신규 요소를 다이어그램으로 나타내보자.

Figure 11.9 Effects through the Element class.

텍스트가 있는 하나의 새로운 요소를 가질 때 generateIndex() 메서드는 그것을 element 변수에 추가하는 동작을 하기때문에 새로운 요소의 등장도 영향을 준다고 볼 수 있다.

이것도 다이어그램으로 나타내보자.

Figure 11.10 generateIndex affecting the elements collection.

지금까지의 작업을 통해 Element 클래스의 addText() 메서드가 element 변수에 영향을 주고, getElement() 메서드와 getElementCount() 메서드에도 영향을 준다는 것을 확인하였다.

텍스트가 제대로 생성되었는 지 검증하려면 getElement() 메서드가 반환한 Element 객체에서 getText() 메서드를 호출하는 방식으로 테스트 코드를 작성할 수 있다.

따라서 getElement() 메서드와 getElementCount()메서드가 변경의 영향을 감지할 수 있는 최적의 위치임을 추론하였다.

3. 영향 전파(Effect Propagation)

코드 내에서의 영향 전파는 기본적으로 아래 세 가지 방법으로 이루어진다.

  1. 호출 코드가 사용하는 반환 값
  2. 인자로 전달된 객체의 상태를 변경하는 경우
  3. 정적 변수 또는 전역 변수를 변경하는 경우

이 영향이 전파되는 과정을 찾기 위해, 일반적으로 반환하는 값이 있는 메서드를 먼저 찾아보는 경우가 많다.

이는 어떠한 변경이 반환되는 값에 영향을 미칠 확률이 높기때문이다.

반면 다른 객체를 매개변수로 받아 상태를 변경하여 애플리케이션에 나머지 부분에 영향을 미치는 은밀한 변경(Effects can also propagate in silent) 도 존재한다.

아래 예제를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Element { 
private String name;
private String text = "";

public Element(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void addText(String newText) {
text += newText;
View.getCurrentDisplay().addText(newText); // HERE
}
public String getText() {
return text;
}
}

이 예제는 전방 추론에서 본 Element 클래스의 코드에서 한 줄만 추가된 코드이다.

1
public void addText(String newText) {}

위의 메서드 시그니처만 보면 View 객체에 어떤 영향을 미치는 지 전혀 추론할 수 없다.

변경이나 정보를 은닉하는 것이 필요할 수도 있지만, 알 필요가 없는 경우에만 적용하는 것이 바람직하다.

변경으로 인한 영향 전파를 찾는 프로세스는 아래와 같다.

  1. 변경 대상인 메서드를 식별한다.
  2. 메서드가 반환 값을 가지는 경우, 이 메서드를 호출하는 코드를 살펴본다.
  3. 메서드가 어떤 값을 변경하는 지 검증해본다. 메서드가 값을 변경하는 경우 그 값을 사용하는 메서드와, 해당 메서드를 사용하는 메서드를 살펴본다.
  4. 객체의 변수 및 메서드를 호출할 수 있는 슈퍼클래스가 서브클래스를 살펴본다.
  5. 메서드에 전달되는 진달되는 인자를 살펴본다. 변경하고자하는 코드가 인자나 반환 값 혹은 반환 객체를 변경하는 지 검증한다.
  6. 식별된 메서드 중에서 전역 변수나 정적 변수를 변경하는 것이 있는지 살펴본다.

4. 영향 추론을 위한 도구(Tools for Effect Reasoning)

우리가 개발하면서 가장 중요한 것 중 하나가 프로그래밍 언어에 대한 지식이다.

어떠한 언어든 영향 전파를 막기위한 일종의 방화벽이 존재하므로, 방화벽의 존재를 알고 있따면 굳이 사용하지않을 이유가 없다.

먼저 Java 언어를 기준으로 변경으로 인한 영향을 파악해보자.

아래 코드는 Coordinate 클래스의 코드로, 현재 내부의 코드를 변경하고 싶다고 가정한다.

요구사항은 3차원 및 4차원 좌표를 표현할 수 있도록 Coordinate 클래스를 일반화하고, 벡터를 사용해 x값과 y값을 유지하고자하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Coordinate { 
private double x = 0;
private double y = 0;

public Coordinate() {}
public Coordinate(double x, double y) {
this.x = x;
this.y = x;
}

public double distance(Coordinate other) {
return Math.sqrt(Math.pow(other.x - x, 2.0) + Math.pow(other.y - y, 2.0));
}
}

위 코드는 캡슐화가 되어있으므로, 클래스 외에 다른 것을 살펴볼 필요가 없다.

만약 아래와 같이 변경한다면 어떨까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Coordinate { 
double x = 0; // HERE
double y = 0; // HERE

public Coordinate() {}
public Coordinate(double x, double y) {
this.x = x;
this.y = x;
}

public double distance(Coordinate other) {
return Math.sqrt(Math.pow(other.x - x, 2.0) + Math.pow(other.y - y, 2.0));
}
}

변수 xy의 접근제어자를 private에서 package로 변경하였다.

private으로 지정되었을 때에는 xy에 어떠한 변경을 수행하든 distance() 메서드를 통해서만 영향을 감지할수 있었으나,

package로 지정되었을 때는 직접적으로 xy에 접근할 수 있게 되므로, 직접 접근하는 코드를 찾아서 살펴보거나 다시 private으로 지정해주어야 한다.

또한 Coordinate 클래스의 서브클래스도 xy에 접근할 수 있는 것은 동일하므로, 서브클래스의 동작들도 살펴볼 수 있다.

이처럼 Java는 접근제어자에 따라 영향의 범위가 달라지고, 덩달아 파악하는 범위도 달라진다.

이번엔 C++ 언어의 예제를 살펴보자.

1
2
3
4
5
6
class PolarCoordinate : public Coordinate { 
public:
PolarCoordinate();
double getRho() const;
double getTheta() const;
};

메서드 뒤에 const 키워드가 부여되어, 해당 메서드는 객체의 변수를 변경할 수 없다는 것을 확인할 수 있다.

만약 여기서 PolarCoordinate 클래스의 슈퍼클래스인 Coordinate 클래스가 아래와 같이 작성되어있다고 가정해보자.

1
2
3
4
class Coordinate {
protected:
mutable double first, second;
};

C++에서는 mutable키워드가 변수 선언에 사용되면 const로 지정된 메서드도 수정할 수 있음을 의미한다.

이처럼 영향을 파악하려면 슈퍼클래스와 서브클래스의 선언부까지 잘 파악해보아야 한다.

5. 영향 분석을 통한 학습(Learning from Effect Analysis)

영향 분석을 할 수 있는 기회가 있다면 미루지말고 바로 해보는 것이 좋다.

프로젝트의 소스에 어느정도 익숙해지면, 직관적으로 영향성이 없다라고 판정할 수 있는 경우가 있는데 이는 프로젝트의 나름의 규칙이 있을 것이기 때문이다.

이 규칙을 발견하는 최고의 방법은 코드의 한 부분이 우리가 본 적 없는 방식으로 영향을 주는 케이스를 발견하는 것이다.

규칙이란 건 프로그래밍의 원칙이라기 보다는 대체로 코드의 맥락과 관련된 것이 많다.

아래 예제를 보자.

1
2
3
4
5
6
7
8
9
10
public class CppClass { 
private String name;
private List declarations;

public CppClass(String name, List declarations) {
this.name = name;
this.declarations = declarations;
}
// ...
}

앞서 이 예제를 보았을 때, 생성자를 통해 전달한 declarations 리스트를 생성 이후 변경하는 것에 대해서 다루었다.

이는 “바보같은 방식”의 대표적인 사례로 생성자에 전달되는 declarations 리스트가 절대 변하지않는 것임을 알고 있다면 파악할 필요가 없기 때문이다.

이처럼 특정 코드 내에서 영향을 줄일 수록 코드를 이해하는 비용이 감소하고 작성 또한 쉬워지는 효과가 있다.

6. 영향 스케치의 단순화(Simplifying Effect Sketches)

위에서 다룬 CppClass의 영향 다이어그램을 살펴보자.

Figure 11.11 Effect sketch for CppClass.

다이어그램을 통해 declarations 리스트와 리스트 내 값들이 메서드들에 영향을 미치는 것을 확인할 수 있는데, 가장 적합한 테스트 루틴의 위치도 바로 추론할 수 있다.

getDeclarationCount() 메서드와 getDeclaration() 메서드는 감지가 불가능한 영역을 getInterface() 메서드는 감지할 수 있기 때문이다.

따라서 getInterface() 메서드에 대한 테스트 코드만 작성하는 것으로 마무리할 수도 있겠지만, 아무래도 getDeclarationCount() 메서드와 getDeclaration() 메서드가 테스트에 포함되지 않는 것이 찜찜함으로 남늗나.

만약 getInterface() 메서드의 코드가 아래와 같다면 어떨까?

1
2
3
4
5
6
7
8
9
public String getInterface(String interfaceName, int [] indices) { 
String result = "class " + interfaceName + " {\npublic:\n";
for (int n = 0; n < indices.length; n++) {
Declaration virtualFunction = (Declaration)(declarations.get(indices[n]));
result += "\t" + virtualFunction.asAbstract() + "\n";
}
result += "};\n";
return result;
}

기존 코드를 아래와 같이 변경하는 것이다.

1
2
3
4
5
6
7
8
9
public String getInterface(String interfaceName, int [] indices) { 
String result = "class " + interfaceName + " {\npublic:\n";
for (int n = 0; n < indices.length; n++) {
Declaration virtualFunction = getDeclaration(indices[n]); // HERE
result += "\t" + virtualFunction.asAbstract() + "\n";
}
result += "};\n";
return result;
}

getDeclaration() 메서드를 getInterface() 내에서 사용하도록 바꾼다면 영향 다이어그램은 아래와 같이 바뀐다.

Figure 11.12 Effect sketch for CppClass.

기존 CppClass 클래스의 영향 다이어그램이 위와 같다면

Figure 11.13 Effect sketch for Changed CppClass.

변경 후 CppClass 클래스의 영향 다이어그램이 위와 같이 그려진다.

단 한 줄만 바꿨음에도 영향 다이어그램상 getInterface() 메서드의 테스트 범위가 getDeclaration() 메서드까지 커버하게 된 것이다.

이처럼 중복 부분을 제거하면 영향 다이어그램서의 종점(endpoint) 개수가 줄어들며, 이는 테스트 구별이 좀 더 간단해졌음을 뜻하기도 한다.

마무리

테스트 루틴을 작성할 위치를 찾을 때는 변경에 의해 무엇이 영향을 받는지 파악하고 추론해야 한다.

간단하게 추론할수도 있고, 본 포스팅에서처럼 영향 다이어그램을 그려가며 세세하게 추론할 수도 있다.

영향 다이어그램을 그리는 것이 번거롭긴 하겠지만, 복잡한 코드를 대상으로 작업하는 경우 테스트 루틴의 위치를 추론하기 위한 몇 안되는 기법이다.