•
테스트하기 편리한 프로그램을 완성하는 유일한 방법은 개발 과정에서 테스트 루틴 작성을 병행하거나 테스트를 지원하는 설계를 위해 많은 시간을 투자하는 것뿐이다.
◦
저자의 생각에 전자의 경우, 희망적인 측면이 많은 반면, 후자의 경우, 그간의 사례를 빌어 그다지 성공적인 것 같지 않다.
•
저자의 경우, 코드를 테스트하는 시도를 통해 다른 관점에서 코드를 바라보기 시작할 수 있었다. 이처럼 코드를 다른 시각으로 바라보는 것은 새롭고 익숙하지 않은 프로그래밍 언어로 작업할 때, 많은 도움을 준다.
봉합
•
단위 테스트를 위해 개별 클래스를 추출하려면 대부분의 경우, 수많은 의존 관계를 먼저 덜어낼 필요가 있다.
◦
흥미로운 것은 아무리 좋은 설계에 기반하고 있다하더라도 꽤 많은 작업이 수반된다는 점이다.
•
테스트를 하기 위해 제품 코드로부터 클래스를 추출하는 작업을 하다 보면 좋은 설계란 무엇인지에 대한 기존의 생각이 달라지게 된다. 또한 소프트웨어를 기존과 전혀 다른 측면에서 바라보게 될 것이다.
◦
프로그램을 커다란 종이 위에 적힌 문자들의 나열이라고 생각하지 않게 되기 때문이다.
◦
그럼 프로그램을 어떤 관점에서 바라봐야 할까?
•
봉합 지점이란 코드를 직접 편집하지 않고도 프로그램의 동작을 변경할 수 있는 위치를 말한다. 아래 예제를 봐보자.
public class MessageSender {
public void send(String type) {
...
if (type.equals("오류") {
ExceptionThrower.throwError();
}
...
}
}
public class ExceptionThrower {
public void throwError() {
throw new Exception();
}
}
Java
복사
•
MessageSender를 테스트해야 할때, send 메소드가 ExceptionThrower라는 객체에 의존성을 가지고 있다. 하지만 우리는 이 테스트의 범위를 해당 클래스만으로 제한하고 싶을 때 어떻게 할 수 있을까?
•
봉합 기법은 MessageSender에 throwError 메소드를 추가하고 테스트 시에, throwError 메소드를 재정의한 테스트 객체를 사용하는 것이다.
public class MessageSender {
public void send(String type) {
...
if (type.equals("오류") {
throwError();
}
}
public void throwError() {
throw new ExceptionThrower.throwError();
}
}
public class TestMessageSender extends MessageSender {
@Override
public void throwError() {
throw new ExceptionThrower.throwError();
}
}
Java
복사
•
이렇게하면 해당 객체가 발생시키는 사이드 이펙트를 고려하지 않고 테스트를 해볼 수 있다.
◦
전역 객체일 경우, 그 영향이 더 클 수 있다.
•
이와 같은 봉합을 저자는 객체 봉합이라 부르며, 이 경우 호출하는 코드를 변경하지 않고 호출되는 메소드만 변경할 수 있다. 객체 봉합은 객체지향 언어에서 사용해볼 수 있다.
•
그렇다면 봉합을 왜 써야하는 걸까? 무슨 의미를 가지는 것일까?
•
수많은 의존 관계를 봉합 지점에서 다른 것으로 대체할 수 있다면 의존 관계를 배제하여 테스트 범위를 한정적으로 제한시킬 수 있기 때문이다.
◦
테스트 시, 의존 관계가 늘어나면 늘어날수록 실패 지점을 찾기가 어려워지고 디버깅도 어렵다.
•
이러한 작업을 통해 데체적으로 충분한 테스트를 준비할 수 있고 좀 더 적극적인 테스트를 수행할 수 있다.
봉합의 종류
•
사용할 수 있는 봉합의 종류는 프로그래밍 언어에 따라 달라지는데, 봉합의 종류를 완벽히 이해하는 가장 좋은 방법은 컴파일되는 단계를 일일이 따라가는 것이다.
◦
단계마다 새로운 종류의 봉합을 발견할 수 있을 것이다.
전처리기 봉합
•
C, C++와 같이 매크로 전처리기를 사용하는 언어에 한정되는 이야기라 불필요하다 판단하여 건너뛰었습니다.
링크 봉합
•
많은 프로그래밍 언어에서는 컴파일만으로 빌드가 완료되지 않는다. 컴파일러는 코드의 중간 표현을 생성하며, 이 중간 표현은 다른 파일에 들어있는 코드를 호출한다.
•
이때 링커는 중간 표현들을 조합한다. 링커가 호출 주소를 변환해주기 때문에 완전한 실행 프로그램을 만들 수 있게 된다.
◦
자바의 경우, 이러한 작업을 컴파일러가 내부적으로 수행한다.
•
언어별로 주소 변환을 수행하는 방법은 다르지만, 일반적으로 링크 봉합 기법을 사용해 프로그램의 일부를 대체할 수 있다.
•
이는 임포트하고 있는 대상 클래스나 함수에 대해 별도의 라이브러리를 작성한 후, 테스트용으로 빌드 스크립트를 변경하여 원래의 라이브러리에 링크시키는 것이다.
•
이는 수고를 필요로 하지만 코드 내의 여러 위치에 존재하는 서드파티 라이브러리를 호출하는 경우 많은 도움이 된다.
⇒ 뭔가 쓸 것 같지는 않지만 알아두면 좋을 수 있는 방법일 것 같다. 고대의 프로그래머들은 이런식으로 의존성을 제한했구나 싶은?
객체 봉합
•
객체 봉합은 객체 지향 언어에서 사용할 수 있는 봉합 기법들 중 가장 유용하다.
cell.recalculate();
Java
복사
•
이 코드를 보면 recalculate라는 메소드가 존재하고 호출을 통해 이 메소드가 호출되어 실행될 것임을 짐작할 수 있다. 그런데 문제는 동일한 이름의 메소드가 중복으로 존재할 수 있다는 점이다.
abstract class Cell {
...
}
class ValueCell extends Cell {
...
}
class FormulaCell extends Cell {
...
}
Java
복사
•
이 상황에서 cell.recalculate()를 호출한다면 어느 메소드가 호출될 까?
•
cell 변수가 어느 객체를 가리키는지 모르면 어느 함수가 호출될지 알 수 없다. 이처럼 다른 코드의 변경 없이 호출되는 recalculate 메소드를 변경할 수 있다면 해당 위치를 봉합 지점이라고 부를 수 있다.
•
다만 다음 같은 상황은 봉합 지점이 아님을 인지하자.
public void etc() {
Cell cell = new Cell();
cell.recalculate();
}
Java
복사
◦
Cell 변수의 클래스는 객체가 생성될 때 결정되므로 메소드를 수정하지 않는 한 cell 클래스를 변경할 수 없다.
◦
이때 활성화 지점이 없기 때문에 봉합 지점이 존재하지 않는다.
•
다음과 같은 상황은 봉합 지점이다.
public void etc(Cell cell) {
cell.recalculate();
}
Java
복사
◦
이는 어떤 cell이든 Cell을 구현하는 클래스가 제공되어 대체될 수 있기 때문이다.
◦
여기서 활성화 지점은 etc의 인수 리스트다. 어떤 종류의 객체를 전달할지 결정하고 테스트에 필요하다면 메소드의 동작을 변경할 수 있기 때문이다.
•
다음은 좀 까다롭다. 다음은 봉합 지점이 존재하는가?
public class Etc extends Etb {
public void et(Cell cell) {
...
recalculate();
...
}
private static void recalcualte(Cell cell) {
...
}
}
Java
복사
◦
et 내의 recalculate 호출은 봉합에 해당한다. et 메소드를 변경하지 않고도 호출 동작을 변경할 수 있기 때문이다.
◦
다음과 같이 봉합해볼 수 있다.
public class Etc extends Etb {
public void et(Cell cell) {
...
recalculate();
...
}
protected void recalcualte(Cell cell) {
...
}
}
public class TestEtc extends Etc{
@Override
protected void recalcualte(Cell cell) {
...
}
}
Java
복사
•
테스트를 수행할 때, 적절한 종류의 봉합을 찾는 것은 중요하다. 일반적으로 객체 봉합은 객체 지향 언어에서 가장 적합한 방법이다.
◦
전처리기 봉합이나 링크 봉합도 편리할 때가 있지만 객체 봉합만큼 분명하지는 않다. 게다가 이들에 의존하는 테스트 루틴은 관리하기도 어렵다.
▪
이들은 의존 관계가 매우 복잡하거나 다른 대안이 없을 경우에 사용하는 것이 바람직하다.
•
봉합이라는 관점에서 코드를 바라보는 것이 익숙해지면 테스트 방법이나 테스트 친화적인 코드 구조를 고안하는 방법을 더 쉽게 찾을 수 있게 된다.