Unit Testing : 4. 좋은 단위 테스트의 4대 요소

    들어가기 전

    이 글은 Unit Testing 4장을 공부하며 작성한 글입니다. 

     

     

    4. 좋은 단위 테스트의 4대 요소

    이 장에서는 좋은 단위 테스트를 구성하는 요소들과 예시들에 대해서 살펴보고자 한다. 좋은 단위 테스트 스위트의 특성은 다음과 같다.

    • 개발 주기에 통합되어 있다. 실제로 사용하는 테스트에만 가치가 있다.
    • 코드베이스의 가장 중요한 부분만을 대상으로 한다. 모든 실행 코드에 똑같이 신경쓸 필요가 없다. 
    • 최소한의 유지비로 최대 가치를 끌어낸다. 

    이것을 만족시킬 수 있는 단위 테스트들을 좋은 단위 테스트라고 한다. 아래에서 계속 살펴보겠다. 

     


    4.1 좋은 단위 테스트의 4대 요소 자세히 살펴보기

    좋은 단위 테스트에는 다음 네 가지 특성이 있다. 각 절에서 아래 특성과 관련된 내용에 대한 상세 설명을 살펴보려고 한다. 

    • 회귀 방지
    • 리팩터링 내성
    • 빠른 피드백
    • 유지 보수성 

     

     

    4.1.1 회귀 방지

    좋은 단위 테스트 코드는 회귀 방지를 해야한다. 회귀는 코드를 수정한 후, 기능이 의도대로 동작하지 않는 소프트웨어 버그를 의미한다. 코드는 자산이 아니고 책임이다. 노출되는 코드 베이스가 많아질수록 회귀(버그)가 발생할 가능성이 더욱 커진다. 그렇기 때문에 회귀에 대해 효과적인 보호를 개발하는 것이 중요하다. 즉, 회귀를 방지할 수 있는 테스트 코드를 작성하자. 

    단위 테스트 코드가 회귀 방지에 좋은 역할을 하는지는 어떻게 알 수 있을까? 아래 세 가지 지표를 이용해서 평가해 볼 수 있다. 

    • 테스트 중에 실행되는 코드의 양
    • 코드 복잡도
    • 코드의 도메인 유의성

    테스트 중에 실행되는 코드의 양이 많을 수록 회귀를 잡아낼 가능성이 높아진다. 그렇지만 이 회귀가 정말로 버그인지를 확인하는 작업도 중요하다. 예를 들어 동일한 입력을 넣었을 때, 동일한 출력값이 나오는데도 테스트가 실패하는 경우가 있다. 반대로 출력값은 바뀌는데 테스트가 실패하지 않는 경우가 있다. 이런 경우를 잘 구분해서 이해해야한다. 

    코드의 복잡도, 도메인 유의성도 중요하다. 복잡한 비즈니스 로직을 나타내는 코드가 보일러플레이트 코드보다 훨씬 더 중요하다. 비즈니스에 중요한 기능에서 발생한 버그가 가장 큰 피해를 입히기 때문이다. (보일러플레이트 코드는 큰 수정없이 다른 영역에서 공통으로 사용되는 코드를 의미한다) 

    가장 높은 수준의 회귀 방지를 위해서는 사용하고 있는 라이브러리, 프레임워크, 외부 시스템을 테스트 범주에 포함시켜서 소프트웨어가 이러한 의존성에 대해 검증이 올바른지를 확인해야한다. 단위 테스트는 메서드 단위가 아니라 동작 단위로 테스트 하는 것을 의미하기 때문이다. 

     

    4.1.2 두번째 요소 : 리팩토링 내성

    리팩토링은 식별할 수 있는 동작을 수정하지 않고 기존 코드를 변경하는 것을 의미한다. 클라이언트가 바라봤을 때는 변경점이 없지만, 구조 개선등을 위해서 내부에서 비기능적인 요소들을 수정하는 작업인데 주로 가독성과 코드 복잡도를 개선하기 위해서 하는 작업이다. 

    좋은 단위 테스트 코드가 되기 위해서는 이런 리팩토링에 영향을 받지 않도록 해야한다. 리팩토링에 테스트 코드가 영향을 받는다는 것은 테스트 코드가 테스트 대상의 내부 동작과 너무 결합되어있음을 의미한다.  리팩토링에 영향을 받는다는 것은 어떤 것을 의미할까? 

    리팩토링을 한 코드를 테스트하면 실패하는 경우가 발생한다. 그렇지만 리팩토링 전후로 입/출력은 동일하기 때문에 기능 자체는 정상적으로 동작한다. 기능은 정상적으로 동작하는데 테스트가 실패하는 경우를 거짓 양성이라고 한다. 정상인 것을 비정상이라고 하는 것이다. 

    거짓양성은 문제가 될까? 당연한 이야기지만 문제가 된다. 우선 단위 테스트의 목적은 지속 가능한 코드의 성장을 지원하는 것이다. 지속 가능한 코드의 성장은 회귀 없이 주기적으로 리팩토링하고 새로운 기능을 추가할 수 있는 것을 의미한다. 적절한 단위 테스트는 아래 두 가지 장점을 제공한다.

    • 기존 기능이 고장 났을 때, 테스트가 조기 경고를 제공한다.
    • 코드 변경이 회귀로 이어지지 않을 것이라는 확신을 제공한다. 

    만약 거짓 양성이 지속적으로 발생한다면 위의 두 가지 장점을 모두 없앤다.

    • 테스트가 타당한 이유로 실패하지 않기 때문에 코드 문제에 대응하는 능력과 의지가 희석된다.
    • 거짓 양성이 빈번히 테스트를 실패하게 만들면, 테스트 코드를 안정망으로 인식하지 않게 된다. 

     

    4.1.3 거짓 양성의 원인

    리팩토링에서 발생하는 거짓 양성의 원인은 테스트 코드의 작성 방식과 관련되어 있다. 거짓양성은 다음 경우에 주로 발생한다.

    • 테스트 대상(SUT : System Under Test)의 내부 구현과 테스트 코드가 많이 결합할수록 거짓 양성이 많아진다.

    주로 화이트 박스 테스트가 많이 작성되어 있으면 거짓 양성이 많이 발생하게 된다. 화이트 박스 테스트는 내부 구현과 결합된 내용들을 테스트 하기 때문이다. 

    따라서 거짓 양성을 줄이는 가장 좋은 방법은 테스트 대상의 내부 구현과 테스트 코드의 결합을 낮추는 방법뿐이다. 결합을 낮추는 것은 테스트 대상의 기능 검증에만 집중하는 것이다. 즉, 클라이언트(사용자) 관점에서 테스트 대상이 기대한 결과를 내주는지를 검증해야한다. 

    아래 테스트 코드들에서 테스트가 테스트 대상의 내부 구현과 많이 결합되어 있는 테스트를 먼저 살펴보려고 한다. 

    public class Team {
    
        private List<Player> players = List.of(new BaseBallPlayer(), new FootBallPlayer(), new SoccerPlayer());
    
        public List<Player> getPlayers() {
            return players;
        }
    }

    Team 클래스가 있고, Team 클래스는 getPlayers()라는 메서드를 제공한다.

    • getPlayers()의 메서드는 현재 팀에 소속된 선수들을 가져오는 것이 기능이다. 
    • Team은 생성될 때 내부적으로 BaseBall, FootBall, SoccerPlayer 선수를 가진다. 

    위의 내용을 고려해서 아래 테스트를 바라보면 어떨까?

    @Test
    void testGetPlayerListBad() {
    
        Team sut = new Team();
    
        List<Player> players = sut.getPlayers();
    
        assertThat(players.get(0)).isInstanceOf(BaseBallPlayer.class);
        assertThat(players.get(1)).isInstanceOf(FootBallPlayer.class);
        assertThat(players.get(2)).isInstanceOf(SoccerPlayer.class);
    }

    위 테스트는 테스트 대상 객체의 내부 구현과 강하게 결합한 테스트다. 왜냐하면 아래 내용이기 때문이다.

    • Team이 반환해주는 PlayerList에 어떤 선수가 순서대로 들어가있는지를 검증한다. 

    이렇게 결합된 테스트 코드는 아주 깨지기 쉬운 테스트 코드가 된다. 예를 들어 팀이 생성될 때, Player List가 빈 상태로 생성될 수도 있고 선수의 순서가 바뀌도록 리팩토링을 할 수도 있다. 이 경우 위 테스트는 바로 실패한다. 그렇지만 getPlayers()의 기능에 문제가 있을까? getPlayers()의 기능에는 문제가 없다.

    getPlayers()의 기능은 Team에 소속된 선수 리스트를 가져오는 것이다. 선수는 매번 바뀔 수도 있고, 선수가 하나도 없을 수도 있다. 따라서 어떤 선수가 어디에 속해있는지를 검증하는 것은 기능을 검증하는 것이 아니고, 사용자에게는 더욱더 의미가 없다. 

    위 테스트 코드의 검증을 살펴보면 다음과 같다. 

    • 각 내부 구현이 기대한 것처럼 동작하는지 확인한다. 
    • 하지만 테스트 대상 시스템의 결과를 검증하지는 않는다. 

    위의 두 가지가 이 테스트 코드의 가장 큰 문제점이다. 따라서 이 단점을 수정해야 리팩토링에 내성을 가진 좋은 테스트 코드가 될 수 있다. 

     

    4.1.4 구현 세부 사항 대신 최종 결과를 검증해라.

    테스트를 깨지지 않게 하고 리팩토링 내성을 높이는 방법은 SUT의 구현 세부 사항과 테스트 간의 결합도를 낮추는 것뿐이다. 코드의 내부 작업과 테스트 사이를 가능한 한 관계없이 만들고 최종 결과만을 테스트하고 검증하는 것이다. 위 테스트 개선한 코드를 아래에서 확인해 볼 수 있다.

    @Test
    void testGetPlayerListGood() {
    
        Team sut = new Team();
    
        List<Player> players = sut.getPlayers();
    
        assertThat(players).isNotNull();
    }
    • getPlayers()의 관찰 가능한 결과는 선수 리스트를 정상적으로 반환하는지다. 따라서 반환되는 값이 Null 객체인지를 확인하면 된다. 

    최종 사용자에게 의미 있는 결과인 '선수 리스트가 반환되는지'만을 검증하기 때문에 최종 사용자에게 의미있는 테스트가 된다. SUT 내부 구현과 테스트가 최대한 분리 되었기 때문에 리팩토링 내성이 더욱 강화된다. 

    테스트와 관련된 부분은 이렇게 개선된다. 

    • 이전 테스트 코드에서는 1,2,3 각 단계가 정상인지를 확인했다. 이것은 기능 검증이라기 보다는 내부 검증에 가깝다. 화이트 박스 테스트에 가깝다.
    • 현재 테스트 코드에서는 1,2,3 단계를 전혀 고려하지 않는다. 출력 결과가 정상인지만을 검증한다. 블랙박스 테스트에 가깝다. 

     

    추가 

    내부 구현과 격리된다고 해서 항상 리팩토링 내성을 가지는 것은 아니다. 예를 들어 getPlayers()에 전달되는 매개변수가 생기는 경우에는 테스트가 실패한다. 하지만 이것은 컴파일 되는 시점에 알 수 있는 에러이기 때문에 즉시 수정하면 된다. 문제가 되는 거짓 양성들은 런타임에만 알 수 있는 거짓 양성들이다. 

     

     


    4.2 회귀 방지, 리팩토링 내성의 직접적인 관계

    좋은 단위 테스트의 요소 중 회귀 방지와 리팩토링 내성은 직접적인 관계가 존재한다. 이 절에서는 이 부분에 대해서 알아보고자 한다. 


    4.2.1. 테스트 정확도 극대

    테스트와 관련된 부분을 살펴보면 다음과 같이 정리할 수 있다.

    잘 작동하는 것이 테스트에 통과, 고장난 것이 테스트에 실패하는 것은 올바른 추론이다. 그렇지만 아래의 경우는 테스트의 신뢰도를 낮춘다. 

    • 고장난 기능이 테스트를 통과하는 경우 (거짓 음성)
    • 기능은 정상인데 테스트에 실패하는 경우 (거짓 양성) 

    위 두 가지 경우를 최소화 하는 것으로 테스트의 정확도를 올릴 수 있다. 그렇다면 거짓 음성과 거짓 양성은 어떻게 최소화 할 수 있을까? 

    거짓 양성은 리팩토링 내성을 개선하면 많은 부분이 해결된다. 즉, 거짓 양성을 리팩토링 내성 개선을 통해 참 음성으로 바꿀 수 있게 된다. 거짓 음성은 회귀 방지를 개선하면 많은 부분이 해결된다. 즉, 테스트 코드에서 거짓을 거짓이라고 이야기 해 줄 수 있도록 코드를 작성하면 된다. 

    바꿔 이야기 하면 좋은 단위 테스트의 4가지 요소들 중에서 회귀 방지와 리팩토링 내성은 테스트 스위트의 정확도를 극대화하는데 사용된다. 

     


    4.2.2 거짓 양성과 거짓 음성의 중요성: 역학관계 

    거짓 음성은 프로젝트 초기부터 꾸준히 테스트 스위트에 큰 영향을 준다. 왜냐하면 틀린 것은 틀리다고 이야기를 해야하기 때문이다. 그렇지만 거짓 양성은 프로젝트 초기에는 중요하지 않다. 왜냐하면 프로젝트 초기에는 코드를 리팩토링 하는 경우가 많지 않기 때문이다. 

    그렇지만 거짓 양성은 프로젝트가 진행될 수록 테스트 스위트에 많은 영향을 주게 된다. 프로젝트가 커지면 커질수록 구조를 정리해야한다. 즉, 리팩토링을 해야한다. 리팩토링을 하면 할수록 리팩토링 내성에 영향을 받게 되고, 이에 따라 거짓 양성 특성 역시 테스트 스위트에 많은 영향을 끼치게 되는 것이다. 

    대부분의 단위 테스트는 회귀 방지에만 중점적으로 작성된다. 하지만 회귀 방지에만으로는 프로젝트 성장을 유지하는데 도움이 되는 정확한 테스트 스위트 구성에는 부족하다. 따라서 항상 리팩토링 내성도 함께 생각해서 단위 테스트를 작성해야한다. 


    4.3 빠른 피드백, 유지 보수성

    이 절에서는 좋은 단위 테스트의 요건 나머지 두 가지 요소를 살펴본다.

    • 빠른 피드백
    • 유지 보수성 

    테스트가 빠르게 동작한다면, 더 자주 돌려볼 수 있다. 더 자주 돌려볼 수 있다는 것은 더 많이 회귀(버그)를 찾을 가능성을 제공해준다. 따라서 단위 테스트는 빠르게 동작하면 소프트웨어에게 좋은 영향을 준다. 

    유지 보수성은 유지비와 관련된 내용이다. 유지비와 관련된 내용은 아래와 같다. 

    • 테스트가 얼마나 이해하기 어려운가? 
      • 테스트는 코드 라인이 적을수록 더 읽기 쉽다. 작은 테스트는 필요할 때마다 변경하는 것도 쉽다. 
    • 테스트가 얼마나 실행하기 어려운가?
      • 테스트가 프로세스 외부 종속성으로 작동하면 DB 서버를 재부팅하고, 네트워크 연결 문제를 해결해야하는 등 의존성을 해결하는데 많은 시간이 필요하다. 

     


    4.4 이상적인 테스트를 찾아서 

    좋은 단위 테스트의 특성을 다시 살펴보면 다음과 같다. 

    • 회귀 방지
    • 리팩토링 내성
    • 빠른 피드백
    • 유지 보수성 

    좋은 단위 테스트를 평가하는 지수는 [회귀 방지 x 리팩토링 내성 x 빠른 피드백 x 유지 보수성]으로 정량화 할 수 있다. 곱의 연산이기 때문에 0에 가까운 지수가 들어가는 순간, 단위 테스트의 효용가치는 0이 된다. 따라서 단위 테스트의 4가지 요소 중 어느 것 하나도 소홀히 할 수 없다. 

    테스트 코드를 포함한 모든 코드는 책임이다. 따라서 테스트 코드에는 정말 가치 있는 테스트 코드들만 테스트 스위트에 남겨두어야 한다. 소수의 매우 가치있는 테스트는 다수의 가치없는 테스트보다 더욱 더 가치있다.


    4.4.1 이상적인 테스트를 만들 수 있는가? 

    결론부터 말하면 회귀 방지 / 리팩토링 내성 / 빠른 피드백 / 유지 보수성을 모두 만족시키는 이상적인 테스트는 만들 수 없다. 회귀 방지 / 리팩토링 내성 / 빠른 피드백은 트레이드 오프 관계를 가지고 있기 때문이다. 이 셋들 중 하나를 희생해야 나머지 둘을 최대로 할 수 있기 때문에 4가지 요소 모두를 좋게 만들 수는 없다. 

     


    4.4.2 극단적인 사례 : End To End 테스트

    End To End 테스트는 최종 사용자의 관점에서 시스템을 살펴본다. 일반적으로 UI, DB, 외부 어플리케이션을 포함한 모든 시스템 구성 요소를 거치게 된다.

    End To End 테스트의 장점 : 회귀 방지 + 리팩토링 내성 

    • End To End 테스트는 많은 코드를 테스트 하기 때문에 회귀 방지에 유용하다. 직접 작성한 코드 뿐 아니라 외부 라이브러리, 프레임워크, 서드파티 어플리케이션 같은 코드를 가장 많이 수행한다. 따라서 근본적으로 기능 수행 관점에서의 에러를 잡아낸다. 
    • End To End는 리팩토링 내성에도 탁월하다. 리팩토링은 식별할 수 있는 동작에는 영향을 미치지 않고, End To End는 식별할 수 있는 동작을 구현하기 때문이다. 
    •  

    End To End 테스트의 단점 : 빠른 실행

    • Ent To End 테스트는 많은 컴포넌트를 테스트에 참여시킨다. 따라서 빠른 실행이 불가능하기 때문에 근본적으로 모든 코드 베이스를 End To End 테스트로 테스트 할 수는 없다. 

     


    4.4.3 극단적인 사례2. 간단한 테스트

    간단한 테스트는 '단순해서 고장이 없을 것 같은 작은 코드 조각'을 테스트 하는 테스트다. 간단한 테스트는 항상 통과하거나 검증이 무의미하기 때문에 어떤 것도 테스트 한다고 볼 수 없다. 

    장점

    • 테스트 실행 속도가 빠르다. 
    • 단순하기 때문에 리팩토링 내성도 우수하다.

    단점

    회귀 방지가 취약하다. 간단한 코드에서는 실수할 여지가 많지 않기 때문이다.

     

    간단한 테스트의 예시 코드는 아래와 같다. 테스트의 의미가 없는 것처럼 보인다. 

    public class UserEntity {
    
        private String Name;
    
        public String getName() {
            return Name;
        }
    
        public void setName(String name) {
            Name = name;
        }
    }
    
    
    class UserEntityTest {
    
        @Test
        void getterSetterTest() {
            UserEntity user = new UserEntity();
            user.setName("hello");
            assertThat(user.getName()).isEqualTo("hello");
        }
    
    }

    4.4.4 극단적인 사례3. 깨지기 쉬운 테스트

    실행이 빠르고 회귀를 잡을 가능성이 높지만, 거짓 양성(리팩토링 내성이 낮은)이 많은 테스트를 만들기 쉽다. 이런 테스트를 깨지기 쉬운 테스트라고 한다. 깨지기 쉬운 테스트는 주로 구현 세부 내용과 많이 결합되어있다. 

    아래 테스트 코드가 깨지기 쉬운 테스트의 전형적인 모습이다. 

    class UserRepositoryTest {
    
        @Test
        void getByIdExecutesCorrectSQLCode() {
    
            UserRepository sut = new UserRepository();
    
            UserEntity user = sut.getById(5);
    
            assertThat(sut.getLastExecutedSqlStmt()).isEqualTo(
                    "select * from dbo.[USER] WHERE UserID = 5");
        }
    }
    
    public class UserRepository {
    
        private String lastExecutedSqlStmt;
    
    
        public UserEntity getById(int id) {
            /*
    
             */
            return null;
        }
    
        public String getLastExecutedSqlStmt() {
            return lastExecutedSqlStmt;
        }
    
        public void setLastExecutedSqlStmt(String lastExecutedSqlStmt) {
            this.lastExecutedSqlStmt = lastExecutedSqlStmt;
        }
    }

    이 테스트 코드는 회귀방지를 훌륭히 수행할까? 회귀방지를 물론 훌륭히 수행한다. 잘못된 것을 잘못되었다고 얘기해 줄 수 있다. 예를 들어 쿼리문에 UserId가 아닌 ID를 입력했다고 가정해보자. 그러면 이 SQL문은 잘못된 SQL 문이기 때문에 위 테스트 코드는 훌륭히 회귀 방지를 해준다. 

    이 코드는 리팩토링 내성에 강할까? 그렇지 않다. 동일한 결과를 수행하지만 약간 수정된 쿼리를 사용할 경우, 기능은 정상적으로 동작하는데 테스트는 실패한다고 나온다. 예를 들어 다음과 같이 쿼리를 수정해도 아무런 문제없이 동작한다.

    SELECT * FROM dbo.User WHERE UserId = 5

    따라서 이 코드는 리팩토링 내성이 약하다. 리팩토링 내성이 약한 이유는 세부 구현 사항인 쿼리문을 검증했기 때문이다. 이 테스트는 '어떻게'에 중점을 맞추고 있기 때문에 문제가 발생한다. 


    4.4.5 이상적인 테스트를 찾아서 : 결론

    테스트의 중요한 요소는 회귀 방지 / 리팩토링 내성 / 빠른 피드백 / 유지 보수성이다. 유지 보수성은 End To End 테스트를 제외하면, 회귀 방지 / 리팩토링 내성 / 빠른 피드백과 관련이 없다. 그렇다면 회귀 방지 / 리팩토링 내성 / 유지 보수성을 골고루 올리는 테스트 코드를 작성해야한다. 

    리팩토링 내성은 포기할 수 없다. 항상 리팩토링 내성을 중점적으로 코드를 작성해야한다. 왜냐하면 리팩토링 내성은 '리팩토링 내성이 있거나 없는' 특성을 가지기 때문이다. 없으면 없고, 있으면 있는 상태고 어중간한 상태는 없기 때문에 항상 리팩토링 내성을 가진 테스트를 생성해야한다. (앞서서 테스트의 효용은 각 테스트 요소의 곱이라고 했다.) 

    반면에 회귀 방지와 빠른 피드백에 대한 지표는 서로 절충해서 작성할 수 있다. 따라서 이상적인 테스트를 만드는 원칙을 정하면 다음과 같다.

    1. 리팩토링 내성을 최우선으로 생각한다.
    2. 이후 회귀 방지 / 빠른 피드백을 고려해서 조절한다. 

    이 절에서 이야기 했던 것들을 그림으로 표현하면 위와 같이 정리할 수 있다. 

     


    4.5 대중적인 테스트 자동화 개념 살펴보기

    이 절에서는 테스트 피라미드와 화이트 박스 테스트 / 블랙박스 테스트라는 두 가지 개념을 살펴본다. 

     

    4.5.1 테스트 피라미드 분해

    테스트를 나누고, 그 테스트들로 테스트 피라미드를 구성할 수 있다. 

    • 테스트 피라미드의 가로 : 얼마나 많은 테스트가 존재하는지를 의미한다.
    • 테스트 피라미드의 세로 : 최종 사용자의 동작을 얼마나 유사하게 흉내내는지를 의미한다. 

    피라미드의 상단은 높은 회귀 방지를 보장하고, 하단은 빠른 테스트 실행을 보여준다. 피라미드 테스트의 유형과 앞서 이야기 했던 테스트의 트레이드 오프를 살펴보면 이런 관계가 생성된다. 

    위의 테스트 피라미드 구조를 고려해서 상대적으로 엔드 투 엔드 테스트를 작성하고, 상대적으로 많은 통합 테스트와 단위 테스트를 작성하는 것이 추천된다. 엔드 투 엔드 테스트가 적어야 하는 이유는 빠르게 실행할 수 없고, 외부 시스템도 같이 유지보수 되어야 하기 때문에 많은 비용이 들기 때문이다. 

     

    테스트 피라미드의 예외

    모든 어플리케이션이 비즈니슈 규칙이나 기타 복잡도가 거의 없는 기본적인 CRUD 작업이라면, 테스트 피라미드는 단위 테스트와 통합 테스트의 수가 같고 End To End 테스트가 없는 직사각형처럼 보일 것이다. 

    단위 테스트는 비즈니스 복잡도가 없는 환경에서는 유용하지 않으므로 적게 작성된다. 반면 통합 테스트는 그 가치가 잘 지켜진다. 코드가 아무리 단순하더라도 DB와 같이 다른 하위 시스템과 통합되어 잘 동작하는지 확인하는 것이 중요하다. 이런 경우 통합 테스트가 단위 테스트보다 더 많아져 역피라미드가 될 수도 있다. 

     

     

    4.5.2 블랙박스 테스트와 화이트박스 테스트 간의 선택

    블랙박스 테스트, 화이트박스 테스트는 또 다른 개념의 테스트다. 다음 두 가지 방식이 언제 사용될 것으로 기대되는지 살펴보자. 

    • 블랙박스 테스트 : 일반적으로 명세와 요구사항, 즉 어플리케이션이 무엇을 해야하는지를 중심으로 구축된다. 사용자 관점의 기능을 살펴보는 테스트다.
    • 화이트박스 테스트 : 어플리케이션의 내부 작업을 검증하는 테스트 방식. 요구 사항, 명세에 의해서 생성되는 것이 아니라 내부 구현 소스 코드에 의해서 생성된다. 

     

    화이트 박스 테스트는 철저하게 더 검증을 하지만, 내부 상세 구현과 결합되어 있기 때문에 리팩토링 내성을 가지지 못한다. 즉, 거짓 양성을 많이 내게 될 것이고, 이는 화이트 박스 테스트가 많은 가치를 제공하지 않음을 의미한다. 블랙박스 테스트는 정반대로 동작한다. 

      회귀 방지 리팩토링 내성
    화이트박스 테스트 좋은 나쁨
    블랙박스 테스트 나쁨 좋음

     

    화이트박스 / 블랙박스 테스트에서 테스트 작성 전략은 다음과 같다.

    • 화이트박스 테스트로는 대상 테스트를 분석하는데 사용한다.
      • 코드 커버리지 도구를 이용해 실행되지 않는 분기를 살피고 분석한다.
    • 블랙박스 테스트를 기본으로 선택해라. 모든 테스트(단위 테스트, 통합 테스트, 엔드 투 엔드 테스트_가 시스템을 블랙박스로 보게 만들고 문제 영역에 의미 있는 동작을 확인해라. 

    따라서 위와 같은 형태로 블랙박스 / 화이트박스 테스트를 조합해서 사용하는 것이 좋다고 한다. 


    4장 요약

    • 좋은 단위 테스트에는 네 가지 기본 특성이 있다.
      • 회귀 방지 / 리팩터링 내성 / 빠른 피드백 / 유지 보수성
    • 회귀 방지는 테스트가 얼마나 버그(회귀)의 존재를 잘 나타내는지에 대한 척도다. 테스트 코드가 더 많이 실행될수록 테스트에서 버그가 드러날 확률이 높아진다.
    • 리팩토링 내성은 테스트가 거짓 양성을 내지 않고, 어플리케이션 코드 리팩토링을 유지할 수 있는 정도를 의미한다.
    • 거짓 양성은 허위 경보다. 거짓 양성은 테스트 스위트에 치명적인 영향을 줄 수 있다.
    • 거짓 양성은 테스트 대상 시스템의 내부 구현 세부 사항과 테스트 간의 강결합의 결과다.
    • 회귀 방지와 리팩토링 내성은 테스트 정확도에 기여한다. 
    • 유지 보수성은 두 가지 요소로 구성된다.
      • 테스트 이해 난이도 : 테스트가 작을수록 읽기 쉽다.
      • 테스트 실행 난이도 : 테스트에 관련된 프로세스 외부 의존성은 적을수록 쉽게 운영할 수 있다. 
    • 회귀 방지 / 리팩토링 내성 / 빠른 피드백은 상호 배타적이기 때문에 네 가지 특성 모두 최대 점수를 받는 것은 불가능하다. 
    • 리팩토링 내성은 타협할 수 없다. 
    • 테스트 피라미드는 단위 테스트, 통합 테스트, 엔드 투 엔드 테스트의 일정한 비율을 일컫는다. 엔드 투 엔드 테스트가 가장 적고, 단위 테스트가 가장 많다.
    • 피라미드에서는 테스트 유형마다 빠른 피드백과 회귀 방지 사이에서 다른 선택을 한다.
    • 테스트를 작성할 때는 블랙박스 테스트를 사용하라. 테스트를 분석할 때는 화이트박스 테스트를 사용하라. 

    댓글

    Designed by JB FACTORY