간단한 메서드 호출보다 복잡한 형태의 계산 작업에 대한 호출이 필요하다면 어떻게 할까? 계산 작업에 대한 객체를 생성해서 이를 호출하면 된다. Runnable 인터페이스가 대표적인 예이다.
값 객체
공유되어야 하지만, identity (동일성) 은 중요하지 않을 때 겍체를 어떤식으로 설계할까? 객체가 생성될 때 객체의 상태를 설정한다. 그리고, 이 상태가 절대 변할 수 없도록 해라. 이 객체에 대해 수행되는 연산은 언제나 새로운 객체를 반환하게 만들어라. 별칭 문제란, 두 객체가 제 삼의 다른 객체에 대한 참조를 공유하는데, 한 객체가 공유 객체의 상태를 변화시키면, 다른 객체가 영향을 받는 문제이다. 해결하기 위한 방법으로,
의존하는 객체애 대한 참조를 외부로 알리지 않는 방법이다. 그 대신 객체에 대한 복사본을 제공한다. 이 방법의 문제는, 수행 시간이나 메모리 공간 측면에서 비효율적이다. 또한, 공유 객체의 상태 변화를 공유할 수 없다.
옵저버 패턴. 의존하는 객체에 자기를 등록해 놓고, 객체의 상태가 변하면 통지를 받는다. 이 방법의 문제는, 제어 흐름이 어렵고 의존성을 설정하고 제거하기 위한 로직이 지저분해진다.
객체를 덜 객체답게 취급. 객체는 시간의 흐름에 따라 변할 수 있는 상태를 가진다. 그런데, 이러한 특징을 제거해버려라. 이것이 값 객체이다.
모든 값 객체는 동등성을 구현해야한다. 만약,
계약을 표현하는 객체가 두 개가 있는데, 둘이 서로 같은 객체가 아니면 이 둘은 동등한것이 아니라 다른것이다.
하지만, 5프랑 짜리 동전 두개가 있는데, 이 동전들이 동일한 (identity) 동전인지는 중요하지 않다. 5프랑은 5프랑이다. 이들은 동등해야한다.(equality)
테스트는 전체 애플리케이션을 대상으로 하는 것 보다, 작은 스케일로 하는게 좋다. 각각의 테스트는 다른 테스트와 독립적이어야한다. 즉, 문제가 하나면 테스트도 하나만 실패해야하고 둘이면 두개만 실패해야한다.격리된 테스트가 내포하는 특징은, 테스트가 실행 순서에 독립적이게 된다는 것이다. 테스트를 격리하기 위한 작업은, 시스템이 응집도는 높고 결합도는 낮은 객체의 모음으로 구성되도록 한다.
테스트 목록
뭘 테스트 해야하나 ? 시작하기 전에 작석해야할 테스트 목록을 모두 적어둬라. 우선 구현할 필요가 있는 모든 오퍼레이션의 사용 예들을 적어라. 그 다음, 이미 존재하지 않는 오퍼레이션에 대해서는 해당 오퍼레이션의 null 버젼 (아무 일도 하지 않는 버젼) 을 리스트에 적어라. 마지막으로 깔끔한 코드를 얻기 위해 이번 작업을 끝내기 전에 반드시 해야할 리펙토링 목록을 적어라.
테스트 우선
테스트는 언제 작성하는 것이 좋은가 ? 테스트 대상이 되는 코드를 작성하기 직전에 작성해라.
assertion 우선
테스트 작성할 때 단언은 언제쯤 쓸까 ? 단언를 먼저 쓰고 시작하라. 단언을 먼저 작성하면 작업을 단순하게 만들 수 있다. 소켓을 통해 다른 시스템과 통신하려고 한다고 해보자. 통신을 마친 후 소켓은 닫혀 있고, 소켓에서 문자열 abc 를 읽어와야한다고 하자.
이 패턴들은 테스트를 언제 어디서 작성할 건지, 테스트 작성을 언제 멈출지에 대한 것이다.
한 단계 테스트
목록에서 다음 테스트를 고를 때 무엇을 기준으로 골라야 할까? 새로운 무언가를 가르쳐 줄 수 있고, 구현할 수 있는 확신이 드는 테스트를 골라라.
시작 테스트
어떤 테스트부터 시작하는게 좋을까? 오퍼레이션이 아무 일도 하지 않는 경우를 먼저 테스트해라.
설명 테스트
자동화된 테스트가 널리 쓰이게 하려면 어떻게 해야할까? 테스트로 설명을 요청하고 테스트로 설명해라. 예를 들어, 누군가 시퀀스 다이엄드램을 설명하라고 하면, 시퀀스 다이어그램의 모든 요소를 포함하는 테스트케이스를 작성해라.
학습 테스트
외부에서 만든 소프트웨어에 대한 테스트를 작성해야 할 때도 있나? 패키지의 새로운 기능을 처음 사용해보기 전에 할 수 있다. 자바의 모바일 정보 기기 프로파일 라이브러리르 기반으로 뭔가를 만든다고 하자. 그냥 바로 사용하는 대신 API 가 우리 예상대로 실행된다는 것을 확인해줄만한 테스트를 먼저 만들어라. 만약, 테스트가 통과하지 않으면 애플리케이션 역시 실행되지 않을것이므로 애플리케이션을 실행해볼 필요도 없다.
지나치게 큰 테스트 케이스를 어떻게 돌아갈 수 있을까 ? 원래 테스트 케이스의 깨지는 부분에 해당하는 작은 테스트 케이스를 작성하고 그 작은 테스트케이스가 실행되도록 해라. 그 후에 다시 원래의 큰 테스트 케이스를 추가해라.
Mock Object
비용이 많이 들거나 복잡한 리소스에 의존하는 객체를 테스트하려면 어떻게 해야할까 ? 상수를 반환하게끔 만든 속임수 버젼의 리소스를 만들면 된다. 예를 들어, 데이터베이스는 시작 시간이 오래 걸린고 깨끗한 상태를 유지하기 어렵다. 그리고 만약 데이터베이스가 원격 서버에 있다면 테스트 성공 여부가 네트워크 상의 물리적인 영향을 받게 된다. 또, 데이터베이스는 개발 중 많은 오류의 원인이다. 해법은, 진짜 데이터베이스를 사용하지 않는 것이다. 모의 객체를 사용하면 성능과 견고함 이외에, 가독성이 좋아진다. 또한, 가시성에 대해 고민하도록 격려해서 설계에서 커플링이 감소하도록 한다. 하지만 모의 객체를 사용하면, 프로젝트에 위험 요소가 추가된다. 모의 객체가 진짜 객체와 동일하게 동작하지 않으면 어떻게 될까 ? 모의 객체용 테스트 집합을 진짜 객체가 사용 가능해질때 그대로 적용해서 이 위험을 줄일 수 있다.
Crash Test Dummy
호출되지 않을 것 같은 에러코드를 어떻게 테스트할 것인가 ? 실제 작업을 수행하는 대신 그냥 예외를 발생시키기만 하는 특수한 객체를 만들어서 호출한다. 파일 시스템에 여유 공간이 없을 경우 발생할 문제에 대해 테스트를 해보자. 자바의 익명 내부 클래스는 테스트하기 원하는 메서드만이 오류를 발생하게끔 하기 위해 유용하다.
테스트 메서드에에서 예외가 발생하건 말건 tearDown() 이 호출되도록 구현했다. 하지만,테스트가 작동하기 위해선 예외를 작아야한다. 여러 테스트를 실행하고 그 결과를 다음과 같이 보길 원한다. “5개 테스트가 실행됨. 2개 실패. TestCaseTest.testFooBar-ZeroDivide Exception, MoneyTest.testNegation-AssertionError” TestCase.run()이 테스트 하나의 실행 결과를 기록하는 TestResult 객체를 반환하게 만들자.
1 2 3 4 5
// TestCaseTest deftest_result(self): test = WasRun("testMethod") result = test.run() assert("1 run, 0 failed" == result.summary())