본문 바로가기

IT/자바

Test-DrivenDevelopment : 테스트 주도 개발

반응형

테스트 주도 개발 도서를 읽으면서 TDD 개발 방법론을 익히고자 한다. 
 
1부 화폐예제를 읽고 PG 결제 시스템의 결제 승인, 취소, 복합결제 기능을 샘플로 구현하면서 책에서 익힌 방법들을 적용해보았다.
 

카드결제, 계좌결제, 간편결제 객체는 Paymt라는 부모 객체를 상속받으며 주문서비스(OrderService)에서 PaymtType 인터페이스에 정의되어 있는 승인, 취소, 복합결제 기능을 간략하게 구현했습니다. 
 
화폐의 통화 개념과 비슷하게 결제별 타입을 결제수단을 통해 카드, 계좌, 간편으로 구분되도록 했으며, 승인과 취소는 결제 금액을 +, - 처리하도록 구현했고 리스트로 여러결제를 받아 전체 결제금액을 세팅함으로써 복합결제 기능을 대신 표현했습니다.
 
책 설명과 동일하게 동치 비교 테스트, 결제승인, 취소 테스트, 서로 다른 통화 더하기는 복합결제로 테스트 코드를 작성했습니다. 
(샘플 코드는 아해 git 주소로 확인 가능)
 
[샘플코드]
https://github.com/ess234/tdd-pgSample.git 

GitHub - ess234/tdd-pgSample

Contribute to ess234/tdd-pgSample development by creating an account on GitHub.

github.com

 
2부는 파이썬을 이용한 TDD 적용기로 따로 정리하지 않고 넘기겠습니다. 
 


3부 TDD 방법론에 대한 개념 정리입니다. 
 
25장 테스트 주도 개발 패턴
테스트에 대한 전략에 대한 소개 장입니다. 
테스트한다는 것은 무엇을 뜻하는가?
테스트를 언제 해야 하는가? 
테스트할 로직을 어떻게 고를 것인가?
테스트할 데이터를 어떻게 고를 것인가?
 
개발을 하면서 스트레스를 받기 시작한다면 우린 어떻게 그 스트레스를 해소할 수 있을까? 해결 방법으론 테스트함으로써 본인 소스에 대한 불확실성을 줄이는 수밖에 없다. 이때 매번 수동으로 테스트를 할 것인가 ?? 아마 그럼 스스로 테스트하는걸 넘기거나 대충하거나 안하는 경우들이 발생할 것이다. 이런 게으름을 줄이고 스트레스를 해소하는 방법은 자동테스트를 구현하는 것이다.
 
각 테스트는 다른 테스트와 완전히 독립적이여야 한다. 왜냐하면 만약 개발 중인 소스의 영향으로 정상 동작하는 기능에 대한 테스트까지 실패한다면 매번 신규 기능을 개발할때마다 통합테스트를 하는 아주 비효율적인 상황이 펼쳐질 것이다. 
테스트를 격리하기 위해서는 시스템의 응집도를 높이고 결합도는 낮출 수 있는 객체들의 모임으로 시스템을 구성하는 것이다.
 
구현해야 할 것들에 대한 테스트 목록을 미리 작성해야 된다. 구현할 필요가 있는 모든 객체의 사용 예를 적고 이미 존재하지 않는 객체에 대한 삭제 작업을 한다. 마지막으로 작업을 끝내기 전에 반드시 해야되는 리펙토링 목록을 적는다.
 
단, 테스트를 한번에 다 만들어 놓아서는 안된다. 왜냐하면 미리 만들어두면 그 테스트 코드를 변경하기 싫어 테스트 구성에 맞춰 비즈니스 로직을 개발하는 아주 안좋은 상황이 발생할 수 있기 때문이다. 
 
목록을 미리 작성하되 테스트 코드는 각 기능을 구현하기 전에 작성해야 된다. 
즉, 테스트는 테스트 대상이 되는 비즈니스 로직을 구현하기 직전에 작성하는 것이 좋다.
또한 테스트를 작성할때 assert (단언)을 제일 먼저 작성한다. 
 
테스트를 작성할때 테스트 데이터를 너무 산발하면 안된다. 최대한 필요한 데이터를 활용해서 테스트를 진행해야 된다. 그리고 테스트의 데이터는 명확해야 된다. 다른 사람이 테스트 코드를 보더라도 이 테스트 데이터가 어떤 용도로 사용되는건지 한눈에 파악될 수 있어야 한다. 
 
예를 들어 계산로직에 대한 기능 테스트 코드를 작성한다면 assertEquals(new Note(49.25), result)가 아닌 assertEquals(new Note(100/2*(1-0.015), result))처럼 계산식 자체를 기재함으로써 한눈에 어떤 계산을 검증하기 위한 테스트 코드임을 알 수 있어야 한다. 


26장 빨간 막대 패턴
이 장은 언제 테스트를 작성하고 언제 테스트 작성을 멈출건지를 설명하는 장이다.
 
TDD는 상향식 작성과 하향식 작성으로 구분할 수 있다. 
상향식 : 전체의 작은 한 조각을 나타내는 테스트에서 조금씩 붙여 나가는 방식
하향식 : 전체를 기준으로 조금씩 세세한 기능 구현으로 나아가는 방식
 
테스트를 하나 작성한다면 이 객체를 어디에 둘지, 적절한 입력 값은 무엇인지, 입력이 주어졌을때 적절한 출력은 무엇인가에 대한 물음을 한번에 해결할 수 있다. 
 
앞으론 테스트를 통해 설명을 요청하고 기능을 설명할 수 있다. 또한, 외부 API를 연동할 경우 학습테스트를 통해 연동 API에 대한 기능을 선 테스트하는것을 권장한다. 그리고 기능 논의 중에 새로운 아이디어나 다른 부분에 대한 추가 사항이 있다면 테스트 목록으로 작성해둠으로써 누락되는 케이스를 막을 수 있다. 
 
그리고 장애가 발생한 경우 회귀테스트를 통해 해당 장애로 실패하는 케이스를 테스트하고, 장애가 수정되었다고 볼 수 있는 테스트를 작성할 수 있다. 회귀테스트를 통해 사용자가 시스템을 통해 어떤걸 기대했으며, 어떤 부분이 잘못되었는지 추가로 설명할 수 있다.
 
마지막으로 개발이 잘 안풀린땐 휴식과 과감하게 로직을 지우고 새로 개발하는 것도 좋은 방법이 될 것이다. 
 


 
27장 테스팅 패턴
이 장은 좀 더 상세한 테스트 작성법에 대한 소개이다. 
 
한번에 큰 테스트를 작성하다 막힐 경우 그 깨지는 부분에 대한 작은 테스트 케이스를 작성함으로써 해결할 수 있다. 
 
비용이 많이 들거나 복잡한 리소스를 의존하는 객체를 테스트할 경우 모의객체 (Mock)을 이용해서 해결할 수 있다. 예를 등어 데이터베이스에 있는 특정케이스에 대한 데이터를 의존하는 경우 그 테스트 데이터를 만들기 위해 불필요한 로직을 구현하기 보단 간단하게 Mock 객체를 활용하여 로직에 대한 테스트 로직을 작성하는것이 좋다. 
이렇게 할 경우 성능이나 견고함 면에서도 좋지만 해당 테스트가 정확히 어떤 데이터를 테스트하기 위한 코드인지 한눈에 파악할 수 있어 가독성면에서 큰 장점이 있다. 
단, 모의객체가 진짜 객체와 동일하게 동작하지 않으면 안되기 때문에 반드시 진짜 객체 그대로 적용해서 모의 객체를 만들어야 된다. 
 
실제로 호출되지 않은 에러코드에 대한 테스트는 실제로 에러를 발생시키기 어렵기 때문에 그냥 예외를 발생시키는 특수한 객체 (= 크래시 테스트 더미)를 만들어서 호출하도록 하면 된다. 
 
예를 들어 테스트 코드에서 익명 내부 클래스를 사용해서 강제로 예외를 발생시킬 수 있다. 

testFileSystemError(){
	File f = new File("foo") {
    	public boolean  createNewFile() throws IOException {
        	throw new IOException();
        }
    };    
    
    try {
    	saveAs(f);
        fail();
    } catch (IOException e) {
    }
}

 
프로그래밍은 작성된 모든 케이스에 대한 테스트가 성공한 상태로 끝마치는 것이 좋다.
 


 
28장 초록 막대 패턴
빨간불의 테스트 작성이 끝나면 가짜로 초록 불로 테스트 상태를 만드는 것이 좋다. 왜냐하면 뭔가 돌아가는 걸 가진 게 그렇지 않는 것보다 좋기 때문이다. 
 
만약 구현해야되는 기능이 명확하게 떠오른다면 바로 구현을 해서 초록불로 바꿔도 괜찮다. 다만 중간에 생각처럼 쉽게 되지 않는다면 다시 돌아가 가짜로 초록불 상태로 변경해도 된다. 
 
빨강 -> 초록 -> 리펙토링 순으로 TDD를 수행한다는 점만 기억하면 된다. 
만약 객체 컬랙션을 다루는 연산 기능을 구현해야 된다면 어떻게 진행할 것인가? 일단 하나부터 시작하길 권한다. 

public void testSum(){
	assertEquals(5, sum(5));
}

private int sum(int value, int[] values) {
	retrun value;
}

그 다음 하나를 두고 여럿을 추가한다. 

public void testSum(){
	assertEquals(5, sum(5, new int[]{5}));
}

private int sum(int value, int[] values) {
	retrun value;
}

이 단계는 변화를 격리하기 방법으로 테스트 케이스에 인자를 추가하면 테스트 케이스에 영향을 주지 않으면서 구현을 자유롭게 할 수 있다. 
그다음 단일값 대신 컬렉션을 사용하는 로직을 개발한다. 

public void testSum(){
	assertEquals(5, sum(5, new int[]{5}));
}

private int sum(int value, int[] values) {
	int sum = 0;
    
    for (int i = 0; i < values.length; i++) {
    	sumt += values[i];
    }
    
    return sum;
}

마지막으로 안쓰는 단일 값을 삭제하면 된다.

public void testSum(){
	assertEquals(5, sum(new int[]{5}));
}

private int sum(int[] values) {
	int sum = 0;
    
    for (int i = 0; i < values.length; i++) {
    	sumt += values[i];
    }
    
    return sum;
}

 


 
29장 xUnit 패턴
이 장은 테스팅 프레임워크를 위한 패턴이다. 
 
단언 (Assertion)은 코드 동작을 판단하는 것으로 테스트 결과를 내리는데 사람이 아닌 컴퓨터로 판단할 수 있도록 하는 것이다. 
여러 테스트에서 사용하는 객체들을 중복되는 경우 매번 생성하는 것보다 픽스처처리하여 공통화하는 것이 좋다.
픽스처는 setup() 메서드를 통해 테스트 클래스에 동일하게 적용할 수도 있고, 여러 테스트 클래스에서 사용되는 경우라면 별도의 픽스처 클래스를 생성함으로써 좀더 유연하게 호출해서 사용할 수도 있다. 
또한, 외부 자원을 사용하는 픽스처 같은 경우 자원해제를 어떻게 할 것인지 고민될 수도 있다. 이 경우 별도의 해제 메소드 역시 픽스처로 등록함으로써 호출해서 자원을 해제해주면 된다. 
 
테스트 메서드는 최대한 메서드 명으로 어떤 테스트인지 그 의미를 그대로 표현하며 읽기 쉽게 작성해야 된다. 그래서 한글로 테스트 메서드 명을 정의해도 된다. 또한 메서드 상위에 아웃라인(주석)으로 given, when, then으로 테스트 데이터 세팅, 호출, 검증을 상세하게 작성하여 다른 사람도 한번에 해당 테스트를 이해할 수 있도록 해야 된다. 
 
예외 테스트와, 전체 테스트에 대한 설명은 junit 버전에서 assertThrown과 gradle test로 그 역할에 대한 테스트들을 수행할 수 있다.  
 
 
 
http://www.yes24.com/Product/Goods/12246033

테스트 주도 개발 - YES24

Test-Driven Development: By Example아름다운 코드와 즐거운 개발을 위한 테스트 주도 개발테스트 주도 개발은 학계와 업계에서 많은 주목을 받아온 프로그래밍 방법으로, 여러 연구 논문과 실례를 통해

www.yes24.com

 

반응형