본문 바로가기

Better SW Development

TDD 안티패턴(Anti-pattern), 전통적으로 잘못 인식되어 있는 테스트 메소드의 리팩터링

* 내용 논의를 위해 올렸습니다. 책의 일부분이라 문맥적으로 다소 어색할 수 있습니다. :) *


기본적으로 좋은 테스트 케이스는 다음과 같은 규칙을 따른다고 말했다.

 

-       하나의 테스트 케이스는 외부와 독립적이어야 한다.

따라서, 다른 테스트 케이스에 영향을 주거나 받지 않아야 한다.

-       하나의 일관된 시나리오를 갖고 있어야 한다.

 

그런데 우리가 테스트 케이스 정련 작업을 할 때 종종 이 항목을 묘하게 위반하곤 한다. 이제 와서 고백하건대, 본인도 앞에서 마찬가지 작업을 했다. 다만, 한꺼번에 설명하는 것이 부담스러웠고, 다소 논란의 소지가 있는 내용이기에 이 부분까지 미뤘다. 여기까지 왔다면, 당신은 그게 무엇인지 들을만한 자격이 있다. :)

 

다음 코드에 대해 다시 한번 살펴보기 바란다.

 

public class AccountTest {

       private Account account;

      

       @Before

       public void setUp(){

             account = new Account(10000);

       }

      

       @Test

       public void testDeposit() throws Exception {

             account.deposit(1000);

             assertEquals(11000, account.getBalance());

       }

      

       @Test

       public void testWithdraw() throws Exception {

             account.withdraw(1000);

             assertEquals(9000, account.getBalance());

       }

}

어느 부분이 미묘하다고 이야기 하려는 걸까?

 

출금하기(withdraw)를 테스트 하는 메소드를 살펴보자.

 

       @Test

       public void testWithdraw() throws Exception {

             account.withdraw(1000);

             assertEquals(9000, account.getBalance());

       }

 

 

테스트 메소드는 하나의 일관된 시나리오를 갖고 외부에 독립적이어야 하는데, 이 부분만으로는 시나리오가 연결성을 갖지 않는다. setUp을 함께 살펴봐야 문맥적으로 하나의 정상적인 테스트 시나리오가 만들어진다.

 

       private Account account;

      

       @Before

       public void setUp(){

             account = new Account(10000);

       }

       @Test

       public void testWithdraw() throws Exception {

             account.withdraw(1000);

             assertEquals(9000, account.getBalance());

       }

 

이건 AccountTest클래스 내의 다른 테스트 케이스의 경우도 마찬가지이다. 앞 테스트 코드는 안타깝지만, 다음과 같이 적는 것이 문맥적으로는 오히려 더 적절하다는 생각이 든다.

 

public class AccountTest {

 

       @Before

       public void setUp(){

       }

      

       @Test

       public void testDeposit() throws Exception {

             Account account = new Account(10000);

             account.deposit(1000);

             assertEquals(11000, account.getBalance());

       }

      

       @Test

       public void testWithdraw() throws Exception {

             Account account = new Account(10000);

             account.withdraw(1000);

             assertEquals(9000, account.getBalance());

       }

}

 

 

'Account 생성이 중복이잖아요!!'라고 항의를 할 수도 있다. 알고 있다. 중복되는 부분이 존재한다.

'..지금 무슨 소리세요? 중복은 나쁜거고.. 나쁜냄새로 불리며, 버그를 쉽게 만들어 내고, ..'

 

맞는 말이다. 하지만, 테스트 케이스 코드는 접근이 조금 다르다. 정련 시에 중복을 제거하는 것은 맞는데, 그 중복되는 부분이 테스트 시나리오의 일부라면 조금 고민해 볼 필요가 있다. 비록 중복이 되었지만, 각각 독립된 테스트 시나리오에 해당하는 위의 두 테스트 메소드는 앞선 경우보다 가독성이 더 높다. 왜냐하면, 테스트 메소드가 하나의 문맥 측면에서 완결성을 갖기 때문이다.

 

그럼 setUp tearDown같은 테스트 픽스처 메소드는 언제 사용할까? 테스트 시나리오에 참가하고 있는 객체들에게 테스트에 필요한 사전 조건이나 기반 환경을 제공할 때 사용하는 것이 더 어울린다. 이를 테면 다음과 같은 가상의 코드가 그 예에 해당한다.

 

 

public class AccountTest {

 

       Connectoin conn;

 

       @Before

       public void setUp(){

             conn = manager.getConnection();

             Transaction.start(conn);

       }

      

       @Test

       public void testDeposit() throws Exception {

             Account account = new Account(10000);

             account.deposit(1000);

             assertEquals(11000, account.getBalance());

       }

      

       @Test

       public void testWithdraw() throws Exception {

             Account account = new Account(10000);

             account.withdraw(1000);

             assertEquals(9000, account.getBalance());

       }

 

       @After

       public void tearDown(){

             Transaction.end(conn);

       }

}

 

 

테스트 시나리오를 진행하기 위해서 선행되어야 하는 환경과 정리작업에 해당하는 부분을 setUp tearDown으로 분리해 놓았다. 그리고 테스트에 필요한 테스트 재료는 각각의 테스트 메소드에서 준비했다.  중복된 코드는 리팩터링의 대상이 되는 것이 맞지만, 테스트 케이스를 작성할 때는 조금 더 고민해 보는 시간이 필요하다.

 

추가 논의

위 내용은 다소 논란의 여지가 있는 부분이라 생각되어 애자일 커뮤니티인 XPer 메일링리스트(http://groups.google.com/group/xper)에 본 절의 제목과 내용을 그대로 올렸습니다. 논의된 의견 중, 박영록님과 김창준 대표님의 답변은 많은 분들과 공유할 만 한 좋은 접근법이라 생각되어 코드를 정리해 옮겨봅니다.

 

        @Test

        public void testWithdraw() throws Exception {

               account = prepareTenThousandAccount();

               account.withdraw(1000);

               assertEquals(9000, account.getBalance());

        }

 

        private Account prepareTenThousandAccount() throws Exception {

               return new Account(10000);

        }

 

[중복을 제거하기 위해 테스트 픽스처 생성 부분을 메소드로 추출한 박영록님의 테스트 코드]

 

 public  class BaseAccountWithDeltasTest {

         private Account account;

         private int base, delta;

 

         @Before

         public void setUp(){

                base = 10000;

                delta = 1000;

                account = new Account(base);

         }

 

         @Test

         public void testDeposit() throws Exception {

                account.deposit(delta);

                assertEquals(base+delta, account.getBalance());

         }

 

         @Test

         public void testWithdraw() throws Exception {

                account.withdraw(delta);

                assertEquals(base-delta, account.getBalance());

         }

 }

 

[최대한 중복이 줄어들어서 마치 템플릿코드처럼 동작하는 김창준님의 테스트 코드]


본 내용은 XPer 메일링 리스트에서도 이야기중에 있습니다.

해당 글은 여기에서 볼 수 있습니다.