•
테스트와 소프트웨어 설계는 긴밀한 상관관계를 가진다.
•
이 둘은 소프트웨어 개발 프로세스의 핵심적인 부분이며 서로 영향을 주고받으며 공존한다. 다시 말해 이 둘은 상호보완적이다.
•
그 이유는 좋은 소프트웨어 설계와 테스트가 추구하는 목표가 일정부분 같기 때문이다.
•
즉, 테스트가 추구하는 가치와 좋은 설계가 추구하는 가치 사이에 일정 부분 교집합이 존재하는 것이다.
◦
좋은 설계는 시스템이 모듈로 분해되고 각 모듈이 독립적으로 개발될 수 있게 하는 것을 추구한다.
◦
이를 통해 모듈화와 재사용성을 높이고 코드를 더 오래, 자주 사용할 수 있게 하며 나아가 시스템이 유연히 확장될 수 있음을 추구한다.
•
테스트하기 쉬운 코드가 무조건 좋은 설계는 아니지만 높은 확률로 그러하다. 따라서 테스트에 관한 고민이 좋은 설계에 관한 고민을 일정부분 해결해줄 수 있다.
•
이번 장에서는 테스트가 어떻게 좋은 설계를 유도하는지, 그 이유를 살펴보자. 그리고 이를 위해 좋은 설계라고 했을 때 대표적으로 소개되는 SOLID와 테스트의 상관 관계를 살펴보겠다.
1. 테스트와 SRP
•
테스트를 작성함으로써 SRP를 위반하는 코드가 SRP을 지키는 방향으로 변경되는 예시를 살펴보자.
•
우리는 테스트를 작성하면서 테스트 대상 시스템의 의존성을 주입한다.
•
이때 각 메소드 별로 사용되거나 사용되지 않는 의존성들을 확인하며 책임에 대해서 고민해볼 수 있다.
◦
UserService의 login, register 메소드가 각각 EmailSender, ClockHolder를 사용있을 때, login에게는 ClockHolder가 불필요한 의존성이고 register에게는 EmailSender가 불필요한 의존성이다.
◦
이는 책임이 잘못 분배되어있다는 신호로 보고 이들을 분리할 수 있다.
•
이를 테스트 과정에서 확인할 수 있다.
2. 테스트와 ISP
•
EmailSender의 인터페이스가 다음과 같이 정의되어 있다고 생각해보자.
public interface EmailSender {
void sendVerificationRequired(User user);
void sendWelcome(User user);
void sendAdvertisement(User user);
void sendCharge(User user);
}
Java
복사
•
UserService의 login 메소드는 오직 sendVerificationRequired만을 사용한다.
•
그러나 우리가 테스트를 작성하기 위해 DummyEmailSender나 FakeEmailSender를 만드는 경우, 인터페이스의 모든 메소드들을 구현해주어야 한다.
•
이는 인터페이스가 통합되어 있기 때문에 발생하는 문제다. 이때문에 테스트의 관심사 밖에 있는 불필요한 메소드들도 구현해야 한다.
3. 테스트와 OCP, DIP
•
좋은 설계를 갖춘 시스템은 유연하다. 좋은 설계로 개발된 시스템이라면 외부 요구사항이 변경되어 코드를 수정할 때도 변경사항의 범위를 최소화할 수 있어야 한다.
•
따라서 설계 원칙에는 이런 유연성을 강조하는 원칙이 많다. 그중에서도 OCP에 주목해보자.
◦
테스트에서 의존성을 쉽게 대체할 수 있음이 유연한 설계를 가졌다는 증명이 되어줄 수 있다.
•
DIP가 추상화를 뜻하는 것은 아니지만 추상화를 통해서 유연성을 추구할 수 있다.
◦
테스트가 의존성 역전 원칙을 유도하는 예는 의존성이 인터페이스가 아닌 구체 클래스에 의존했을 때, 테스트하기가 쉬워지는 지 어려워지는 지를 통해 증명할 수 있다.
•
단순히, 서비스를 추상에 의존하게 만들면 된다. 역할에 의존하는 코드를 만들면 모든 고민이 해결 된다.
4. 테스트와 LSP
•
테스트에 대해 많은 주제를 이야기 했으니 이제 어떤 것을 테스트해야 하지에 대한 의논해보자.
•
이 질문의 답변은 꽤나 다양할 것인데, 그중에서 특히나 많이 듣는 답변은 Right-BICEP과 CORRECT 원칙이다.
Right-BICEP
•
Right: 결과가 올바른지 확인해 봐야 한다.
•
Boundary: 경계 조건에서 코드가 정상적으로 동작하는지 확인해야 한다.
•
Inverse: 역함수가 있다면 이를 실행해 입력과 일치하는지 확인해야 한다.
•
Cross-Check: 검증에 사용할 다른 수단이 있다면 이를 확인해야 한다.
•
Error Conditions: 오류 상황에서도 프로그램이 의도한 동작을 하는지 확인해야 한다.
•
Performance: 프로그램이 예상한 성능 수준을 유지하는지 확인해야 한다.
CORRECT
•
Conformance(적합성): 데이터 포맷이 제대로 처리되는지 확인해야 한다.
•
Ordering(정렬): 출력에 순서가 보장되어야 한다면 이를 확인해야 한다.
•
Range(범위): 입력에 양 끝점이 있다면 양 끝점이 들어갈 때 정상 동작하는지 확인해야 한다.
•
Reference(참조): 협력 객체의 상태에 따라 어떻게 동작하는지 확인해야 한다.
•
Existence(존재): null, blank와 같은 값이 입력될 때 어떻게 반응하는지 확인해야 한다.
•
Cardinality(원소 개수): 입력의 개수가 0, 1, 2, … n개 일때 어떻게 동작하는지 확인해야 한다.
•
Time(시간): 병렬 처리를 한다면 순서가 보장되는지 확인해야 한다.
•
사실 이보다 더 간단한 답변이 있다. 유지하고 싶은 상태, 즉 변경되지 않았으면 하는 상태가 있다면 테스트로 작성하라는 말이다.
•
어떤 시스템에 특정한 테스트 케이스가 없다면 해당 케이스는 시스템에서 변경되어도 상관없는 것일 수 있기 때문이다.
•
유지하고 싶은 상태가 있다면 테스트로 작성하라.