I Can’t Get This Class into a Test Harness
이전 포스팅들을 통해 클래스를 테스트 하네스에 넣는 작업이 쉽지않다는 것을 깨달았을 것이다.
아래는 가장 일반적으로 직면하는 네 가지 문제점이다.
- 클래스의 객체를 쉽게 생성할 수 없다.
- 클래스를 포함하는 테스트 하네스를 쉽게 빌드할 수 없다.
- 반드시 사용해야하는 생성자가 부작용을 야기한다.
- 생성자 호출시 많은 처리나 연산이 발생하며, 해당 내용을 알아내야한다.
본 포스팅에서는 테스트 하네스에 클래스를 넣을 때 겪을 수 있는 문제를 크게 7가지의 케이스로 분류하여 알아보도록 한다.
1. The Case of the Irritating Parameter (성가신 매개변수)
테스트를 위해 객체 생성을 시도할 때, 특정 매개변수가 필요한 경우가 있다.
객체 생성에 필요한 매개변수를 일일이 확인하는 것은 매우 성가신 일이지만 해결해야하는 문제이기도 하다.
예제를 통해 알아보자.
아래 CreditValidator
클래스는 가상의 신용카드 청구 시스템으로, 테스트 코드가 작성되지않은 상태라고 가정한다.
1 | public class CreditValidator { |
CreditValidator
클래스는 고객의 신용 잔고가 유효한지 검증하고, 유효하지않다면 예외를 발생시키는 동작을 가지고 있다.
현재 상태를 유지한 채, 새로운 메서드는 getValidationPercent()
를 추가해보자.
이 메서드의 요구사항은 CreditValidator
클래스의 객체가 존재하는 동안 validateCustomer()
메서드 호출이 성공했던 비율을 반환하는 것으로 한다.
일단 무작정 객체 생성에 도전해보자.
1 | public void testCreate() { |
위의 코드에서는 객체 생성에 필요한 RGHConnection
객체, CreditMaster
객체, validatorID
문자열이 없으므로 당연히 컴파일 오류가 발생한다.
인자로 넘겨야하는 객체들의 코드는 아래와 같다.
1 | public class RGHConnection { |
RGHConnection
클래스의 객체는 생성된 후, 서버에 연결을 시도하여, 고객의 신용을 검증하는 데 필요한 정보를 내려받는다.
1 | public class CreditMaster { |
CreditMaster
클래스의 객체는 생성된 후, 파일로부터 고객의 신용 잔고 결정에 사용되는 정보를 읽어온다.
RGHConnection
클래스와 CreditMaster
클래스의 생성 정보를 토대로 테스트 코드를 보강해보자.
1 | public void testCreate() throws Exception { |
이렇게 하면 validator
객체를 생성할 수는 있지만, connection
객체가 서버와의 연결을 시도하는 문제점이 있다.
서버와의 연결은 무조건 성공한다고 보장되지도 않으며, 성공은 하지만 오랜 시간이 걸릴 수도 있기 때문이다.
반면 master
객체의 경우, 기존에 존재하는 파일을 불러오는 것이므로 파일만 정상적으로 존재한다면, 문제될 게 없다.
파일이 없어서 생기는 문제의 경우, 테스트 환경이 아니라도 실제 환경에서 발생할 것이기 때문이다.
따라서 validator
객체를 생성시 문제가 되는 것은 RGHConnection
클래스이다.
이를 우회하기 위해서 RGHConnection
클래스의 위장 객체를 만들어서 서버와의 연결 고리를 끊어버리면 된다.
이제 위장 객체를 만들기 위해 RGHConnection
클래스의 UML을 살펴보자.
RGHConnection
클래스는 서버와의 연결을 위해 connect()
, disconnect()
, retry()
메서드를 가지고 있으며, 비즈니스 로직을 처리하기 위해 RFDIReporterfor()
, ACTIOReportFor()
메서드도 가지고 있다.
최초 정의한 요구사항인 CreditValidator
에서의 메서드 추가를 위해선 RFDIReporterfor()
를 호출해서 필요한 정보를 얻어야 한다고 가정해보자.
실제 프로덕트에서는 서버로부터 받아와야겠지만, 테스트 하네스 안에스는 서버와의 연결 고리를 끊을 것이기때문에 다른 방법으로 정보를 획득해야 한다.
먼저 인터페이스 추출 기법을 이용해 위의 UML대로 IRGHConnection
인터페이스를 분리한 뒤 위장 객체 FakeConnection
클래스를 작성해보자.
1 | public class FakeConnection implements IRGHConnection { |
이제 위장 객체 FakeConnection
클래스를 이용해 테스트 코드를 다시 작성해보면 아래와 같다.
1 | void testNoSuccess() throws Exception { |
이제 CreditValidator
클래스의 객체를 생성할 수 있게 사전 작업이 완료되었다.
최초의 요구사항인 getValidationPercent()
이 있다는 전제하에 테스트 코드를 작성해보자.
1 | void testAllPassed100Percent() throws Exception { |
위의 테스트 코드는 한 개의 유효한 고객 정보를 받아 검증 가능 비율이 100% 임을 확인할 수 있는 코드이다.
여기까지 왔다면 한 가지 의문이 생긴다.
1 | CreditMaster master = new CreditMaster("crm2.mas", true); |
위의 코드는 정말 필요한 것일까?
getValidationPercent()
메서드는 master
객체를 전혀 사용하지않는다.
따라서 아래와 같이 CreditValidator
객체 생성시 아래와 같이 처리해도 된다.
1 | // as-is |
물론 위와 같이 null을 전달하는 것은 테스트 루틴에 한정해서 작성해야한다.
실수로 null을 전달한다 하더라도, 런타임 에러를 통해 어느 부분에서 오류가 발생했는지 바로 특정할 수 있다.
즉, 테스트 루틴내에서 인자가 실제로 사용되는 지 여부를 빠르게 검증해낼 수 있다.
좀 더 빠르게 이를 파악하고 싶다면, 테스트 하네스 안에서 무작정 객체 생성을 시도할 때 아래와 같이 작성하면 될 것이다.
1 | public void testCreate() { |
이처럼 인터페이스 추출 기법과 null값 전달은 성가신 인자 관련 문제를 해결할 수 있는 기법들이다.
참고 물론 서브클래스를 작성해
connect()
메서드를 재정의하는 식으로 해결할 수도 있지만, 서브클래스로 인해 테스트 대상 코드의 동작이 그대로 보장되는 지(=변경되지않는지) 확인해보아야한다.
2. The Case of the Hidden Dependency (숨겨진 의존 관계)
특별할 것 없는 클래스가 하나 있다고 하자.
근데 이 클래스의 생성자를 호출하는 경우 바로 문제를 직면하게 되는 경우가 있다.
원인 중 가장 흔한 것이 의존 관계가 숨겨져 있는 경우로, 테스트 하네스 내에서 쉽게 접근할 수 없는 자원을 사용하는 경우 쉽게 발생한다.
메일링 리스트를 관리하는 예제를 통해 파악해보자.
1 | class mailing_list_dispatcher { |
위의 mailing_list_dispatcher
클래스의 생성자의 일부분은 아래와 같다.
1 | mailing_list_dispatcher::mailing_list_dispatcher() |
생성자의 초기화 목록에서 new
키워드를 사용해 mail_service
객체를 할당하고 있는데, 이로인해 문제가 더욱 악화된다.
또한 몇 가지 작업 수행 외에는 의미를 알 수 없는 client_type
변수 또한 존재하고 있다.
물론 테스트 루틴안에서 mailing_list_dispatcher
클래스의 객체를 생성할 수는 있다.
하지만 매번 메일 라이브러리에 연결해야하고 메일 시스템을 설정해야 하며, 테스트 도중 send_message()
메서드를 호출하면 실제로 메일이 전송되어 버린다.
시스템의 전반적인 테스트를 하는 것이라면 상관없겠지만, 지금은 기존에 존재하는 코드에 테스트가 완료된 신규 기능을 추가하는 것이 목적이다.
상술한 것처럼 여기서 근본적인 문제는 mail_service
에 대한 의존 관계가 mailing_list_dispatcher
클래스의 생성자 내부에 숨어있다는 점이므로, mail_service
의 위장 객체를 생성할 수 있다면 문제점은 우회할 수 있을 것이다.
이때 사용할 수 있는 기법이 생성자 매개변수화 기법(Parameterize Constructor) 이다.
생성자 매개변수화 기법을 사용하면 mail_service
객체를 생성자에 전달함으로써, 생성자 내부의 의존 관계를 밖으로 드러나게 만들 수 있다.
해당 기법을 적용한 코드는 아래와 같다.
1 | mailing_list_dispatcher::mailing_list_dispatcher(mail_service *service) |
기법 이라는 단어까지 썼지만, 생성자의 인자가 추가된 것 외엔 그다지 큰 차이점이 없다.
하지만 외부로 의존 관계를 드러나게함으로써, 인터페이스 추출 기법을 사용해 main_service
의 인터페이스를 정의한 위장 객체를 만들 수 있게 된 것이다.
생성자 매개변수화 기법은 생성자의 의존 관계를 외부화하는 매우 쉬운 방법이지만, 많은 사람들이 이 기법을 잊어버리곤 한다.
그 이유는 새롭게 추가된 인자를 전달하기 위해 클래스를 호출하는 모든 코드를 변경해야한다고 생각하기 때문이다.
물론 맞는 말이다. 하지만 초기화를 위한 메서드를 추가하고, 생성자의 구현부를 옮기는 것으로 이 문제를 우회할 수 있다.
이번엔 mailing_list_dispatcher
클래스에 initialize()
메서드를 추가해보자.
1 | void mailing_list_dispatcher::initialize(mail_service *service) { |
이러한 방식은 메서드 추출 기법과 달리, 생성자의 시그니처를 유지할 수 있으므로 테스트 없이도 안전하게 사용할 수 있다.
1 | mailing_list_dispatcher::mailing_list_dispatcher() { |
위와 같이 한 번 더 변경하면 기존 코드와 시그니처까지 똑같기 때문에 호출하는 쪽에서 변경사항을 전혀 알 필요가 없다.
3. The Case of the Construction Blob (복잡한 생성자)
생성자 매개변수화 기법은 매우 쉽고 좋은 방법이긴 하지만, 모든 경우에 최적인 기법은 아니다.
생성자 내부에서 많은 수의 객체가 생성되거나, 많은 수의 전역 변수에 접근하는 경우 매개변수의 개수가 덩달아 늘어날 것 이기 때문이다.
심지어 생성자 내부에서 몇 개의 객체를 생성한 뒤, 이 객체들을 다른 객체를 생성하는 데 사용되는 경우도 있다.
아래 코드가 그 예시이다.
1 | class WatercolorPane { |
위의 코드에서 cursor
변수를 통해 감지 작업을 수행하려고 하는 경우 문제가 발생한다.
cursor
변수에 초기화된 FocusWidget
클래스의 경우 생성을 위한 코드까지 전부 생성자에 있기 때문이다.
FocusWidget
클래스의 생성에 필요한 코드를 외부로 분리할 수 있다면, FocusWidget
객체를 먼저 생성한 후 생성자에 파라미터로 넘겨줄 수 있을 것이다.
다만 테스트 루틴이 준비되지않았다면 위와 같은 변경의 안정성을 보장할 수 없다.
또 다른 선택지로 인스턴스 변수 대체 기법(Supersede Instance Variable) 이 있다.
인스턴스 변수 대체 기법이란 객체를 먼저 생성한 후, 다른 인스턴스로 대체하기 위한 Setter를 클래스에 추가하는 기법이다.
참고 안전하게 메서드를 추출할 수 있는 리팩토링 도구가 있다면 팩토리 메서드 추출 및 재정의 기법(Extract and Override Factory Method) 을 또 다른 선택지로 고려할 수 있지만, 이는 모든 언어에서 동작하진 않는다.
아래 코드를 보자.
1 | class WatercolorPane { |
이제 대체 메서드 supersedeCursor()
메서드를 추가하였으니, WatercolorPane
클래스 외부에서 FocusWidget
클래스의 객체를 전달할 수 있다.
여기서 인터페이스 추출 기법이나 구현체 추출 기법을 FocusWidget
클래스에 적용하여 위장 객체를 전달하면 감지 작업을 수행할 수 있다.
1 | TEST(renderBorder, WatercolorPane) { |
지금까지 예제를 통해 인스턴스 변수 대체 기법에 대해 알아보았다.
이 기법은 대체로 자원 관리에 문제를 일으킬 가능성이 존재하므로 불가피한 경우가 아니라면 차선책으로 두어야한다.
보통 팩토리 메서드 추출과 재정의 기법을 사용하지만, C++ 등의 언어에서 사용할 수 없는 경우 적용하는 것이 좋다.
4. The Case of the Irritating Global Dependency (까다로운 전역 의존 관계)
소프트웨어 업계엔 다수의 상용 또는 오픈소스 프레임워크가 존재한다.
프레임워크는 프로젝트 초반의 효율성을 높여주고 여러가지 문제를 해결해주지만, 프레임워크를 사용한다고 하더라도 애플리케이션을 개발하면서 임의의 클래스를 테스트 하네스에 집어넣고 컴파일하는 것이 쉽지않은 일이다.
특히 전역 변수는 테스트 프레임워크에서 클래스의 생성 및 사용을 어렵게 만드는 가장 까다로운 녀석이다.
간단한 경우엔 생성자 매개변수화 기법(Parameterize Constructor), 메서드 매개변수화 기법(Parameterize Method), 호출 추출과 재정의 기법(Extract and Override Call) 등을 사용하면 되지만 전역 변수와 관련된 의존 관계를 너무 광범위하다는 것이 문제다.
이번 케이스도 마찬가지로 예제를 통해 파악해보자.
아래 코드는 정부 기관이 사용하는 건축 허가 관리를 위한 클래스이다.
1 | public class Facility { |
Facility
클래스의 객체 생성을 무작정 시도해본다.
1 | public void testCreate() { |
testCreate()
테스트 코느는 컴파일 오류 없이 작성되었다.
하지만 내부에서 PermitRepository
클래스를 사용하여 associatedPermit
변수에 Permit
객체를 초기화하고 있기에, 문제가 발생했을 경우 탐지가 어려운 상황에 놓이게 된다.
이를 해결하기위해 생성자 매개변수화 기법을 사용할 수도 있지만 PermitRepository
클래스는 싱글턴 패턴으로 작성되어 Global Scope를 가지고 있기 때문에 사용처가 여러 곳일 수 있다.
프로덕트 입장에서 싱글턴의 사용은 중복 객체를 방지하기 위한 좋은 방법이지만, 테스트 코드 입장에서는 테스트 대상 코드를 하나의 애플리케이션으로 간주하기때문에 외부와의 통로가 남아있는 싱글턴은 일종의 제약 사항으로 남는다.
이를 해결하기 위해 싱글턴에 새로운 정적 메서드를 추가해보자.
1 | public class PermitRepository { |
setTestingInstance()
메서드를 추가하여 새로운 객체를 생성하여 싱글턴에 주입하도록 하였다.
이제 테스트 코드에서 PermitRepository
싱글턴 클래스에 접근하는 경우 setTestingInstance()
메서드를 호출하여 인스턴스를 획득하면 된다.
1 | public void setUp() { |
헌데 이러한 방식은 PermitRepository
클래스의 생성자가 private
로 지정되는 싱글턴 패턴의 법칙에 위배된다.
싱글턴 패턴을 사용하는 즉, 시스템 내에 인스턴스가 하나만 존재해야하는 경우는 아래와 같다.
- 현실 세계를 모델링한 결과, 현실 세계에 한 개만 존재한다.
- 두 개가 존재한다면 심각한 문제가 발생한다.
- 두 개를 생성하면 자원 소모가 극심하다.
하지만 싱글턴 패턴은 위와 같은 거창한 이유보단, Global Scope에서 접근 가능한 전역 변수로서의 활용을 위해 사용하는 경우가 많다.
만약 싱글턴의 특성을 완화해도 되는 경우라면 생성자를 private
에서 protected
, public
, package
으로 변경하는 식으로 테스트 루틴에 활용하는 것도 하나의 방법이다.
예를 들어 특정 허가를 모두 추가할 수 있는 addPermit()
이라는 메서드가 있다면, 별도의 Repository를 생성하여 수행해도 테스트를 수행할 수도 있으며,
테스트 하네스에서 처리되지않길 바라는 동작이 포함되는 경우, 서브클래스화 기법이나 메서드 재정의 기법을 사용해 서브클래스를 작성하여 테스트 루틴을 단순화할 수 있다.
아래는 PermitRepository
클래스에 findAssociatedPermit()
메서드를 추가한 예제이다.
1 | public class PermitRepository { |
- 먼저 permit DB를 연다.
- 사용할 값을 선택한다.
- 단 하나의 알맞은 permit을 갖는지 확인한다. 알맞지않다면 에러를 발생시킨다.
- 일치하는 permit을 반환한다.
위의 절차에서 DB와의 외부 의존관계를 끊어내기 위해 아래와 같이 TestingPermitRepository
서브 클래스를 작성한다.
1 | public class TestingPermitRepository extends PermitRepository { |
서브클래스화 기법을 이용하면 PermitRepository
의 생성자를 protected
로 처리하여 싱글턴의 일부 특성을 유지한 채로 테스트 루틴을 작성할 수 있다.
1 | public class PermitRepository { |
이처럼 서브클래스화 기법과 메서드 재정의 기법을 사용해 싱글턴 위장 객체를 생성할 수 있는 경우가 많다.
하지만 의존 관계가 매우 광범위한 경우 싱글턴에 인터페이스 추출 기법을 적용해 애플리케이션 내의 모든 참조를 인터페이스를 사용하도록 변경하는 것이 나을 수 있다.
후자의 경우 많은 변경으로 그에 비례한 리소스가 필요하지만 컴파일러의 도움을 얻을 수 있을 것이다.
인터페이스를 추출한 후 PermitRepository
클래스는 아래와 같다.
1 | public class PermitRepository implements IPermitRepository { |
IPermitRepository
인터페이스는 PermitRepository
클래스의 public
이면서 static
이 아닌 모든 메서드의 시그니처를 가지고 있어야 한다.
1 | public interface IPermitRepository { |
지금까지 설명한 리팩토링 방법을 정적 set 메소드 도입 기법(Introduce Static Setter) 이라고 한다.
이 기법은 광범위하게 전역 의존 관계가 존재하는 경우에도 테스트 루틴을 적절한 위치에 배치할 수 있다.
다만, 전역 의존 관계를 제거하는 역할은 하지 못하므로 완전히 끊어내려면 메서드 매개변수화 기법이나 생성자 매개변수화 기법을 사용해야한다.
참고 메서드 매개변수화 기법은 메서드가 추가되기때문에 클래스를 이애하는 데 방해가 될 수 있고, 생성자 매개변수화 기법은 현재 전역 변수를 사용중인 모든 객체에 필드가 새로 추가된다는 단점이 있다.
5. The Case of the Horrible Include Dependencies (공포스러운 포함 의존 관계)
테스트 하네스에서 C++ 클래스를 생성하는 것은 헤더의 의존 관계가 있어 매우 어려운 일이다.
테스트 하네스 내에서 클래스를 생성하는 데 필요한 헤더 파일은 어떤 것들이 있을까?
이번에도 예제를 통해 알아보자.
1 |
|
위의 Scheduler
클래스는 200개 이상의 메서드를 포함하고 있다고 가정한다.
Scheduler
클래스는 Meeting
, MailDaemon
, Events
, SchedulerDisplays
, Dates
등의 클래스를 사용한다.
SchedulerTests
테스트 루틴을 작성하여 빌드해보자.
1 |
|
위와 같이 작성하면 include 관련 문제가 발생한다.
Scheduler
클래스를 컴파일하려면 Scheduler
클래스가 의존하고 있는 모든 것들을 컴파일러가 알아야하기 때문이다.
간단하게 모든 정보를 Scheduler.h
파일에 포함시키는 것도 가능하지만 하나의 헤더 파일에 모든 정보를 담는 것도 쉽지않은 일이다.
따라서 테스트 루틴에 객체를 생성하고 사용하려면 지속적으로 include
키워드를 사용해야 한다.
Scheduler
클래스의 모든 전처리 include
를 복사하는 방법도 있지만, 모든 헤더가 필요하지 않을 수도 있으므로 어느 정도의 최적화를 진행해야한다.
단순한 작업이지만, 분량이 많을 수록 혼란을 야기하기도 하고, 의존 관계가 적더라도 테스트 하네스 내에서 접근하기 어려운 경우에도 문제가 될 수 있다.
SchedulerDisplay.h
의 SchedulerDisplay
클래스가 이 예시이다.
1 |
|
SchedulerDisplay
클래스에 displayEntry()
메서드를 추가하였다.
이렇게 만들어진 위장 객체를 별도의 헤더 파일에 작성한 후 테스트 코드 내에 아래와 같이 참조한다.
1 |
|
이러한 작업을 반복하면 상대적으로 간단하게 의존 관계를 주입할 수 있다.
다만 몇 가지 심각한 단점이 있다.
먼저 프로그램을 분리시켜야 하며, 실제로 프로덕션 코드에서는 의존 관계가 제거된 것이 아니다.
이렇게 테스트 파일에 포함된 SchedulerDisplay
클래스에 displayEntry()
메서드와 같은 중복 정의는 테스트를 수행하는 한 계속해서 유지가 되어야 한다.
6. The Case of the Onion Parameter (양파껍질 매개변수)
별다른 파라미터를 넘기지 않고도 객체를 생성할 수 있다면 좋겠지만, 생성 이후의 처리를 가능하게 하기 위해 기존에 생성된 별도의 객체를 넘기는 경우가 많다.
즉, A 객체를 생성하기 위해 B 객체가 필요하며, B 객체의 생성을 위해 C 객체가 필요한 상황을 말한다.
이처럼 객체 내의 또 다른 객체는 양파껍질 벗기기와 비슷하다.
아래 예제를 보자.
1 | public class SchedulingTaskPane extends SchedulerPane { |
위의 코드를 보면 SchedulingTaskPane
객체를 생성하기 위해 SchedulingTask
객체가 필요함을 알 수 있다.
이번엔 SchedulingTask
클래스를 살펴보자.
1 | public class SchedulingTask extends SerialTask { |
SchedulingTask
객체를 생성하기 위해서도 Scheduler
객체와 MeetingResolver
객체가 필요하다.
이러한 구조의 클래스 관계에서는 객체 생성을 위해 원시 타입만을 필요로하는 클래스를 찾아서 계속해서 양파껍질을 벗길 수 밖에 없다.
이 상황을 대처하려면 꼭 필요한 인자가 아닌 경우 Null 전달 기법(Pass Null) 을 사용할 수 있다.
테스트에 몇 개의 동작만이 피룡하다면, 직접적인 의존 관계에 대해 인터페이스 추출 기법(Extract Interface) 이나 구현체 추출 기법(Extract Implementer) 을 사용해 인터페이스를 통한 위장 객체로 대치가 가능하다.
위의 예제에서 SchedulingTaskPane
클래스와 가장 직접적인 의존 관계를 가진 클래스는 SchedulingTask
클래스이다.
즉 SchedulingTask
클래스의 위장 객체를 만들 수 있다면 해결할 수 있을 것이다.
좀 더 자세히 보면 SchedulingTask
클래스가 SerialTask
클래스를 상속받고 있는데, SerialTask
클래스의 메서드까지 포함하는 인터페이스를 작성해 위장객체로 만들 수 있다.
UML로 보자면 아래와 같다.
7. The Case of the Aliased Parameter (별명을 갖는 매개변수)
생성자의 인자가 문제가 되는 경우는 대부분 인터페이스 추출 기법(Extract Interface) 이나 구현체 추출 기법(Extract Implementer) 을 사용해 해결할 수 있다.
“대부분”이라고 썼듯이 모든 경우에 최적은 아닌데, 어떤 케이스에 해당하는지 예제를 통해 살펴보자.
아래 예제는 위에서 다뤘던 허가 시스템의 다른 클래스이다.
1 | public class IndustrialFacility extends Facility { |
위의 코드를 기준으로 테스트 하네스에서 IndustrialFacility
클래스를 생성하는 데 문제점을 나열하면 아래와 같다.
- 싱글턴인
PermitRepository
클래스에 접근한다. - 생성자의
OriginationPermit
객체를 생성하기가 어렵다.
1번 문제는 위의 4번 케이스의 해결 방법대로 처리하면 되지만, 2번 문제는 복잡한 의존 관계가 설정되어있을 경우, 객체 생성에 애로사항이 생긴다.
OriginationPermit
클래스에 인터페이스 추출 기법을 적용하면 될까?
아래 계층 구조를 보면 인터페이스 추출 기법으로 해소되지않음을 유추할 수 있다.
가장 확실한 해결책은 아래와 같이 인터페이스들로만 이뤄진 계층 구조를 생성한 후 Permit
필드를 IPermit
으로 변경하는 것이다.
이렇게 하면 해결은 할 수 있겠지만, 클래스와 인터페이스 간의 일대일 관계는 설계상 혼란을 야기할 가능성이 크다.
다시 OriginationPermit
클래스를 살펴보자.
1 | public class OriginationPermit extends FacilityPermit { |
validate()
메서드는 아래와 같은 절차를 수행한다.
- 데이터베이스 연결
- 정보 검증 질의
- 확인 플래그 설정
- 데이터베이스 연결 해제
이 메서드는 데이터베이스와의 연결을 필요로 하므로, 테스트 중에 실행하지않도록 처리하는 것이 좋다.
이 상황은 서브클래스 기법 과 메서드 재정의 기법 을 고려할만하다.
서브클래스 FakeOriginationPermit
를 선언하고 검증 플래그를 변경할 수 있는 메서드를 추가할 수 있도록 작성한 뒤, 재정의한 validate()
를 IndustrialFacility
클래스의 테스트에 사용하는 방식이다.
코드로 확인해보자.
1 | public void testHasPermits() { |
이처럼 즉석에서 클래스를 작성하여 해결할 수는 있지만, 의존 관계로 인해 선택한 궁여지책이므로 그다지 바람직한 경우는 아닐 것이다.
물론 프로덕션 환경이 아닌 테스트 환경에서 특별한 테스트 케이스를 만들 수 있는 장점이 있긴하다.
이처럼 의존 관계가 복잡한 경우 어떤 메서드 추출을 먼저할지 고민하도록 하자.