[Test] 테스트 주도 개발 (28 ~ 30장)

DaeHoon·2022년 12월 2일
0

TDD

목록 보기
8/9

28. 초록 막대 패턴

가짜로 구현하기(진짜로 만들기 전까지만)

  • 실패하는 테스트를 만든 후 첫 번째 구현은 상수를 반환하게 한다. (리턴 값을 하드코딩)
  • 테스트가 통과하면 단계적으로 상수를 변수를 사용하는 수식으로 변형한다.

삼각측량

  • 예가 2개 이상일 때에만 추상화를 하면 최대한 보수적으로 추상화 과정을 테스트로 만들어 낼 수 있다.
  • 어떤 계산을 어떻게 해야 올바르게 추상화할 것인지에 대해 정말 감잡기 어려울 때에만 삼각 측량을 쓰며, 그 외의 경우에는 명백한 구현이나 가짜로 구현하기에 의존하는 것이 좋다.

명백한 구현

  • ‘제대로 동작하는’을 푸는 동시에 ‘깨끗한 코드’를 해결하려는 것은 한번에 하기에는 너무 많은 일일 수 있다.
  • 우선 ‘제대로 동작하는’ 으로 되돌아가 문제를 해결하고, 그 후에 ‘깨끗한 코드’를 수행하자.

29. xUnit 패턴

단언 (assertion)

  • 불리언(boolean) 수식을 작성해서 여러분 대신 프로그램이 자동으로 코드가 동작하는지에 대한 판단을 수행하도록 하라. -> 결과를 평가할 때 체크해야할 점들을 boolean 수식을 작성하게 만들자.
  • 판단 결과가 불리언 값이어야 한다. 일반적으로 참 값은 모든 테스트가 통과했음을 의미하고, 거짓 값은 뭔가 예상치 못했던 일이 발생했음을 의미한다.
  • 단언은 구체적이어야 한다.
    • assertTrue(rectangle.area() != 0 ) 구체적이지 않은 단언
    • assertTrue(rectangle.area() ==50 ) 구체적인 단언
Contract contract = new Contract();
contract.begin();
assertEquals(Running.class, contract.status.class)
  • 어떠한 객체의 상태를 체크하는 테스트를 작성할 때 위와 같이 코드를 짤 수 있지만, 위의 테스트는 assertEquals로 값이 같은지를 판단해 status에 의존적인 테스트다.

픽스처 (Fixture)

  • 여러 테스트에서 공통으로 사용하는 객체들을 생성할 때 객체를 세팅하는 코드
  • setUp 함수에 픽스쳐를 선언하게 테스트를 작성하면 중복 코드 이슈를 어느정도 해결할 수 있다.
  • 하지만 중복의 좋은 점도 존재하는데, 테스트를 위에서부터 아래로 읽어내려 갈 수 있다. 하지만 픽스처가 별도의 메서드 (setUp)으로 분리되면 나머지 테스트를 작성하기 전에, 메서드가 자동으로 호출된다는 점과 객체들이 어떻게 초기화가 되었는지 기억해야 한다.
private Rectangle empty;

public void setUp(){
	empty = new Rectangle(0,0,0,0) // Fixture
}

public void testEmpty(){
	assertTrue(empty.isEmpty());
}

외부 픽스처

  • 픽스처 중 외부 자원이 있을 경우 tearDown 메서드를 정의하여 자원을 해제한다.
  • 각 테스트의 목적 중 하나는 테스트가 실행되기 전과 후의 외부의 상태가 동일해야 한다는 것이다.
  • 예를 들어, 테스트 중에 파일을 열었다면 테스트가 끝나기 전에 이를 반드시 닫아줘야 한다.

테스트 메서드

  • 동일한 픽스처를 공유하는 모든 테스트는 동일한 클래스의 메서드로 작성될 것이다. 다른 종류의 픽스처를 필요로 하는 테스트는 다른 클래스에 존재하게 된다.
  • 관습에 의해 메서드 이름은 ‘test’로 시작한다. 툴은 이 패턴을 자동으로 인식하고 주어진 클래스에 대한 테스트 슈트를 생성한다.
  • 테스트 메서드는 의미가 그대로 드러나는 코드로, 읽기 쉬워야 한다.

예외 테스트

  • 예상되는 예외를 잡아서 무시하고, 예외가 발생하지 않은 경우에 한해서 테스트가 실패하게 만들면 된다.
public void tesetRate(){
	exchange.addRate("USD", "GBP", 2);
    int rate = exchange.findRate("USD", "GBP");
    assertEquals(2, rate);
}
  • 위와 같은 테스트가 있고, 값이 찾아지지 않으면 예외를 던지고 싶지만 쉽게 방법이 떠오르지 않는다.
public void testMissingRate(){
	try{
	    exchange.findRate("USD", "GBP");
        fail();
    } catch (IllegalArgumentException e){
    	...
    }
}
  • findRate()가 예외를 던지는 로직이 없으면 fail()이 실행될 것이다. fail은 테스트가 실패했음을 알려주기 위한 xUnit 메서드다.

전체 테스트

  • 모든 테스트를 한번에 실행하려면 모든 테스트 슈트에 대한 모음을 작성하면 된다. (각각의 패키지에 대해 하나씩, 그리고 전체 애플리케이션의 패키지 테스트를 모아주는 테스트 슈트)

30. 디자인 패턴

  • TDD에서는 설계를 디자인 패턴과는 조금 다른 관점으로 본다.

TDD에서 디자인 패턴의 쓰임세

패턴테스트 작성리팩토링
커맨드X
값 객체X
널 객체X
템플릿 메서드X
플러거블 객체X
플러거블 셀렉터X
팩토리 메서드XX
임포스터XX
컴포지트XX
수집 매개 변수XX

커맨드

  • 간단한 메서드 호출보다 복잡한 형태의 계산 작업에 대한 호출이 필요할 때 계산 작업에 대한 객체를 생성하여 이를 호출한다.
  • 메세지 하나를 보내는 것보다 호출이 조금 더 구체적이고 또 조작하기 쉬워지려면 객체가 정답. 호출 자체를 나타내기 위한 객체를 만드는 것이다
  • 객체를 생성할 때 계산에 필요한 모든 매개변수들을 초기화한다.
  • 호출할 준비가 되면, run()과 같은 프로토콜을 이용해 계산을 호출한다.
Runnable 
interface Runnable
    public abstract void run();
  • run을 원하는데로 구현하면 되나, 문법적으로 간결한 방법을 제공하지 않기 때문에 자주 쓰이지는 않는다.

값 객체

  • 널리 공유해야 하지만 동일성(identity)은 중요하지 않을 때 객체를 어떤 식으로 설계할 수 있을 까? -> 객체가 생성될 때 객체의 상태를 설정한 후 이 상태가 절대 변할 수 없도록 한다. 그리고 이 객체에 대해 수행되는 연산은 언제나 새로운 객체를 반환하게 만든다.

  • 두 객체가 다른 객체에 대한 레퍼런스를 공유하고 있는데, 한 객체가 공유되는 객체의 상태를 변화시키면 다른 객체는 공유된 객체에서 나온 값들이 쓸모 없게 된다. 차라리 공유 객체의 상태에 의존하지 않게 설계하는 편이 나을 것이다. 이는 고전적인 별칭 문제다.

  • 별칭 문제 해결 방법으로 한 가지는 현재 의존하는 객체에 대한 레퍼런스를 결코 외부로 알리지 않는 방법이다. 그 대신 객체에 대한 복사본을 제공하는 것이다. 또 다른 방법은 옵저버 패턴을 사용하는 것이다. 의존하는 객체에 자기를 등록해 놓고, 객체의 상태가 변하면 통지를 받는 방법이다.

  • 모든 값 객체는 동등성을 구현해야 한다. 참고로 동일성(identity)와 동등성(equality)은 서로 다르다. 예를 들어 5백원 동전 두 개가 서로 동등할지라도 동일하지는 않다.

  • 즉, 값 객체는 해당 객체의 주소 값이 달라도 값(Structural Equality,구조적 동등성)이 같으면 동일하다고 생각 할 수 있다.

널 객체

  • 객체의 특별한 상황을 표현하고자 할때 어떻게 해야할까? -> 그 특별한 상황을 표현하는 새로운 객체를 만들자. 그리고 객체에 다른 정상적인 상황을 나타내는 객체와 동일한 인터페이스나 상위 클래스를 제공하자
public boolean setReadOnly() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkWrite(path);
    }
    return fs.setReadOnly(this);
}
// null 객체
- LaxSecurity
public void canWrite(String path){
}

- SecurityManager
public static SecurityManager getSecurityManager(){
    return security == null ? new LaxSecurity() : security;
}

- File
public boolean setReadOnly() {
    SecurityManager security = System.getSecurityManager();
    security.canWrite(path);
    return fileSystem.setReadOnly(this);
}
  • java.io.file에는 파일을 쓸 때 guard != null이 18번이나 나온다. 이를 매번 검사하는 것 보다는 절대로 예외를 던지지 않는 LaxSecurity 클래스를 만들어 테스트를 한다.

템플릿 메서드

  • 작업 순서는 변하지 않지만 각 작업 단위에 대한 미래의 개선 가능성을 열어두고 싶은 경우 어떻게 표현할 것인가? -> 다른 메서드들을 호출하는 내용으로만 이루어진 메서드를 만든다.

class TestCase{
  public void runBare() throws Throwable {
      setUp();
      try{
          runTest();
      }
      finally {
          tearDown();
      }
  }
}
  • JUnit은 테스트를 실행하기 위한 기본 순서를 위와 같이 구현한다. (AbstractClass)
  • 하위 클래스는 setUp, runTest, tearDown을 구현하면 된다. (ConcreateClass)
  • 템플릿 메서드를 만들 때 한가지 문제는 하위 클래스를 위한 기본 구현을 제공할 것인가 말것인가 하는 것이다. runBare()에서는 세 개의 메서드가 모두 기본 구현을 가지고 있다.

플러거블 객체

  • 변이를 표현하기 가장 좋은방법은 명시적인 조건문을 사용하는 것이다.
if circle: then {
...
} else{
...
}
  • 하지만 이런 코드는 여러 소스에도 똑같이 퍼져나갈 확률이 높다. TDD의 두 번재 수칙이 중복을 제거하는 것이기 때문에, 명시적인 조건문이 전염되는 싹을 애초에 잘라버려야 한다. 이를 위해 플러거블 객체를 끄집어내보자.

class SelectionTool{
  Figure selected;

  public void mouseDown(){
    selected = findFigure();

    if(selected != null){
      select(selected);
    }
  }

  public void mouseMove(){
    if(selected != null){
      move(selected);
    } else{
      moveSelectionRectangle();
    }
  }

  public void mouseUp(){
    if(selected == null){
      selectAll();
    }
  }
}
  • 이게 바로 지저분한 중복 조건문들이다. 이를 플러거블 객체인 SelectionMode를 만들어 해결해보자.
  • SelectionMode는 SingleSelection과 MultipleSelection이라는 두가지 구현을 갖는다.

class SelectionTool{
  SelectionMode mode;

  public void mouseDown(){
    selected = findFigure();

    if(selected != null){
      mode = new SingleSelection(selected);
    } else{
      mode = new MultipleSelection();
    }
  }

  public void mouseMove(){
    mode.mouseMove();
  }

  public void mouseUp(){
    mode.mouseUp();
  }
}
  • 도형 클릭할 때 사용하는 메서드인 mouseDown에서만 조건문을 체크하고, 조건에 맞는 객체를 플러거블 객체를 통해 만들었다.

플러거블 셀렉터

인스턴스별로 서로 다른 메서드가 동적으로 호출되게 하려면 어떻게 해야할까? -> 메서드의 이름을 저장하고 있다가 그 이름에 해당하는 메서드를 동적으로 호출하도록 한다.

abstract class Report{
	abstract void print();
}

class HTMLReport extends Report{
	void print(){..}
}

class XMLReport extends Report{
	void print(){..}
}
  • 단지 메서드 하나만 구현하는 하위 클래스가 10개 정도 있다 가정해보자. 상속은 별로 좋은 방법이 아니다.
abstract class Report{
	String printMessage;
    Report(String printMessage){
    	this.printMEssage = printMessage;
    }
	void print(){
    	switch(printMessage){
        	case "printHTML":
            	printHTML();
                break;
            case "printXML":
            	printHTML();
                break;
               
        	}
    }
    void printHTML(){
    }
   
    void printXML(){
    }
   
}
  • switch 문을 이용한 코드, 단 새로운 종류를 추가할 때마다 switch문을 바꿔야 한다. (OCP에 위반)
void print(){
	Method runMethod = getClass().getMethod(printMessage, null);
    runMethod.invoke(this, new Class[0]);
}
  • 리플렉션을 이용해 어느정도 유연성을 추가했다.

팩토리 메서드

  • 새 객체를 만들기 위해 유연성을 위해 일반 메서드에서 객체를 생성하게 한다.
  • 메서드라는 한 단계의 간접 접근(indirection)을 추가함으로써 테스트를 변경하지 않고 다른 클래스의 인스턴스를 반환할 수 있는 유연함을 얻었다.
class Money{
	static Dollar dollar(int amount){
    	return new Dollar(amount);
    }
}
  • 팩토리 메서드의 단점은 이 간접 접근이다. 유연함이 필요할 때만 사용하고, 그렇지 않으면 생성자를 호출하는 방식으로 코드를 구현하자.

사칭 사기꾼 (Imposter)

  • 기존의 코드에 새로운 변이를 도입하려면 어떻게 해야할까? -> 기존의 객체와 같은 프로토콜(인터페이스 또는 상위 클래스) 을 갖지만 구현은 다른 새로운 객체를 추가한다. (다형성)
testRectangle(){
	Drawing d = new Drawing();
    d.addFigure(new RectangleFigure(0, 10, 50, 100));
    RecordingMedium brush = new RecordingMedium();
    d.display(brush);
    assertEquals(' rectangle 0 10 50 100 \n', brush.log());
}
  • 이런 사각형을 테스트하는 코드를 만들어보자
  • 우리는 이제 타원을 표시하고 싶다. 이 경우 임포스터를 발견하기는 쉽다. RectangleFigure을 OvalFigure로 바꾸기만 하면 된다.
testOval(){
	Drawing d = new Drawing();
    d.addFigure(new OvalFigure(0, 10, 50, 100));
    RecordingMedium brush = new RecordingMedium();
    d.display(brush);
    assertEquals(' Oval 0 10 50 100 \n', brush.log());
}
  • 리팩토링 중에 나타나는 임포스터의 두 가지 예
    • 널 객체: 데이터가 없는 상태를 데이터가 있는 상태와 동일하게 취급
    • 컴포지트: 객체의 집합을 단일 객체처럼 취급할 수 있다.

컴포지트

  • 하나의 객체가 다른 객체 목록의 행위를 조합한 것처럼 행동하게 만들려면 어떻게 해야 할까? 객체 집합을 나타내는 객체를 단일 객체에 대한 임포스터로 구현한다.
public Transaction{
	Transaction(Money value){
    	this.value = value;
    }
}

public Account{
	Transaction transactions[];
	Money balance(){
    	Money sum = Money.zero();
        for (int i =0; i < transactions.length; i++){
        	sum = sum.plus(transactions[i].value);
        }
        return sum;
    }
  	
}
  • Transaction은 값을 갖고 Account는 잔액을 갖는다.
  • 고객은 여러 계좌를 가지고 있고, 전체 계좌의 잔액을 알고 싶어한다. 이를 구현하기 위해 컴포지트 패턴을 사용해보자.
interface Holding{
	Money balance();
}
  • Component 역할의 Holding 인터페이스를 만든다.
// Leaf
class Transaction implements Holding{
	@Override
    Money balance(){
    	return value
    }
}

// Composite
class Account implements Holding{
	Holding holdings[];
    
    @Override
    Money balance(){
	Money sum = Money.zero();
        for (int i =0; i < holdings.length; i++){
        	sum = sum.plus(holdings[i].balance);
        }
        return sum;    
    }
}
  • 그 다음 Composite 객체인 Account에 Leaf를 보관하는 holdings 배열을 선언하여 로직을 처리한다.

수집 매개 변수

  • 여러 객체에 걸쳐 존재하는 오퍼레이션의 결과를 수집하려면, 결과가 수집될 객체를 각 오퍼레이션의 매개 변수로 추가하자.

ex) java.io.Externalizatble 인터페이스 예제

public interface Externalizable extends java.io.Serializable{
	void writeExternal(ObjectOutput out) throws IOException;
}
  • 결과가 수집될 객체 (ObjectOutput)를 매개 변수로 받아서 처리하고 있다.

싱글톤

  • 전역 변수를 제공하지 않는 언어에서 전역 변수를 사용하려면 하지 마라. 전역 변수를 사용하는 대신 설계에 대해 다시 한 번 생각하자.
profile
평범한 백엔드 개발자

0개의 댓글