본문 바로가기

Better SW Development

메시지코드를 이용해 만든 예외(Exception)처리에 대한 단위 테스트

특정 업무 규칙 검증을 위해 하나의 BizException을 만들고 메시지, 혹은 메시지 코드를 이용해 예외처리를 하는 코드를 가끔씩 보곤 합니다. 이를테면 다음과 같은 식으로 코드를 작성한 경우입니다.
    private void checkInvalidate(Purchase purchase){
        if(purchase == null ) throw new PurchaseException("구매 정보가 없습니다.");
       
        if(purchase.getItem() == null) {
            throw new PurchaseException("구매할 상품이 없습니다.");
        }
       
        if(purchase.getItem().isSelling() == false) {
            throw new PurchaseException("판매가 마감되었습니다.");
        }
    }
[소셜 커머스 사이트의 오늘의 딜 구매항목 체크메소드..라고 가정]

이런 경우 작성할 수 있는 테스트 케이스는 몇 가지 종류가 있습니다. 다음은 그 한가지 예 입니다.
    @Test(expected=PurchaseException.class)
    public void testRegister_구매상품_선택_누락() throws Exception {
        //Given
        Purchase purchase = new Purchase(null);
       
        //When
        this.shoppingService.register(purchase);
    }
[구매 상품 누락시 적절한 업무 예외가 발생하는지 여부를 점검하는 코드]

이 정도만 해도 괜찮습니다만, 구매할 상품이 지정되지 않았거나 구매 진행시점에 판매가 마감된 상황등을 테스트할 때도 비슷한 모양의 테스트 코드가 사용되기 때문에 Test의 정밀조준(Pin-point Targeting)이 이루어지지 않습니다.

    @Test(expected=PurchaseException.class)
    public void testRegister_판매마감된_상품_구매() throws Exception {
        //Given       
        Item item = TestFactory.saleClosedItem();
        Purchase purchase = new Purchase(item);

        //When
        this.shoppingService.register(purchase);
    }
[판매가 마감된 경우에 대한 처리를 테스트 하는 코드. 앞의 테스트 코드와 동일한 그물로 잡아낸다.]

위 코드 시리즈의 경우, 머리속으로 예상한 상황과는 다른 이유에 의해 PurchaseException이 발생해도 구분해 낼 방법이 없습니다. 이런 경우를 막기 위해서 메시지, 혹은 메시지 코드를 직접 비교하는 테스트 코드를 작성해 보겠습니다.

    @Test
    public void testRegister_판매마감된_상품_구매() throws Exception {
        //Given       
        Item item = TestFactory.saleClosedItem();
        Purchase purchase = new Purchase(item);

        //When
        try {
            this.shoppingService.register(purchase);           
        } catch (PurchaseException pe) {
            if( !pe.getMessage().equals( "판매가 마감되었습니다." ) ){
                // Then
                fail();
            }
            return;
        }
        //or Then
        fail();
    }
[메시지 (코드)를 직접 비교해서 처리하는 테스트 케이스]

우선 위 테스트 코드는 동작은 하지만 사실 몇 가지 찜찜함이 남습니다. (찜찜하지 않으면 안됩니다!)

- 테스트 코드 내에 if구문을 이용한 메시지 캡처링(Message Capturing) 스타일을 사용했습니다. 이 경우 코드가 복잡해 지거나 테스트 코드 자체에 버그가 생기기 쉽습니다. (이크! 저도 한 번 생겼어요!)

- 또한 fail 발생 조건을 계속 따라가봐야 하는 일이 발생하고 verify또한 명확하지가 않습니다.

어떻게 해야 할까요?

우선 (일반적인 설계자들이 제시하는) 가장 이상적인 방법은 설계를 조금 수정하는 겁니다. 다른 종류의 업무적인 예외상황이 같은 예외타입으로 발생하는 것은 좋지 않으니까, 경우에 맞는 예외클래스를 만들어 냅니다. 즉, 업무 규칙을 분석해서 예외 클래스를 적절히 재 구성해야 합니다.

    @Test(expected=ClosedSaleException.class)
    public void testRegister_판매마감된_상품_구매() throws Exception {
        //Given       
        Item item = TestFactory.saleClosedItem();
        Purchase purchase = new Purchase(item);

        //When
        this.shoppingService.register(purchase);
    }
[새로 만든 예외 클래스 ClosedSaleException. 객체 지향이 의례 그렇듯 조금 더 분화되고 나뉜다]

물론 예외코드를 enum 비슷하게 만들고, 동일한 예외로 쓰면 안될까요? 라고 물을 수도 있는데, 여기서 중요한건 에러코드문제가 아니라 예외발생상황 테스트를 어떻게 할 것이냐이기 때문에 해당 접근은 적절하지 않습니다.

그런데 경우에 따라서는 매번 예외를 분화해서 만들기 곤란하거나 어려운 경우도 있습니다. 그럴때는 JUnit4에서 지원하는 Rule중에서 ExpectedException을 씁니다.

    @Rule
    public ExpectedException thrown = ExpectedException.none();   

    @Test
    public void testRegister_판매마감된_상품_구매() throws Exception {
        //Given       
        Item item = TestFactory.saleClosedItem();
        Purchase purchase = new Purchase(item);

        //Expect
        thrown.expect(PurchaseException.class);
        thrown.expectMessage("판매가 마감되었습니다.");

        //When
        this.shoppingService.register(purchase);

        //Else
        fail();
    }
[ExpectedException을 사용한 경우. Expect, Else로 구분한다]

앞의 if문을 이용한 경우보다 코드가 좀 더 간결해 진 걸 알 수 있습니다. 아쉬운 점은 JUnit의 Rule을 잘 모른다면 써야 할 생각을 떠올리지 못한다는 점입니다. 설계와 개발은 밀접하게 연결되어 있고, 때로는 Low Level 라이브러리와도 밀접하게 영향을 주고 받습니다. 잘 분화된 상위설계, 혹은 디테일한 아키텍처 프레임워크나 라이브러리, 어느 한 쪽만을 가지고는 좋은 설계와 코드가 나오기 어렵습니다. 어떻게 대응해야 할지, 사실 뾰족한 방법이 없습니다. 양쪽 방향을 다 공부하고 학습하고 적용해 보면서 Better way를 찾아볼 수 밖에요.

위에 나열한 코드들도 사실 어떤게 정답이다! 라고 할 수없습니다. 무책임하게 들릴지도 모르겠지만 상황에 맞게 적절히 사용할 수 밖에 없습니다. 상황에 적절함이라는건 또 어떻게 알수 있는 건가?라는 의문이 들텐데요, 마찬가지로 학습을 통해 선임자들이 겪었던 실수의 반복을 줄이고 그 과정에서 얻게 된 스스로의 경험을 통해 이를 수 밖에 없는 것 같습니다.

하지만 경험의 레벨를 떠나 어느 상황이 되었든, 기본적으로 고민하고 노력해야 하는 건, 우리가 작성하는 코드들은 읽기 편해야 한다는 점입니다. 물론 테스트 코드 포함해서 말입니다. 그리고 읽기 편하다는 건 자신에게만이 아니라 다른 사람들에게도 마찬가지여야 하고요.

어쨌든, 이 글은 테스트 코드에 대한 이야기이니 이에 대한 제 경험을 이야기 해 본다면, 복잡한 테스트 코드는 곧잘 더 복잡한 업무코드를 만들어 내는 지름길이 되곤 했답니다. : )

ps.
JUnit4의 Rule에 대해서는, 많은 분들이 좀 더 유용하게 쓰셨으면 하는 마음에 '책 일부 공개'라는 글(http://blog.doortts.com/155)에 PDF를 링크로 걸어놓았습니다. Rule를 포함해서 JUnit4의 몇 가지 고급 기능들에 대한 설명이 들어있으니 한 번 살펴보세요.