ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • nextstep-자동차경주 (테스트하기 어려운 코드 테스트하기)
    언어/JAVA 2023. 7. 23. 09:43

    1. 들어가기전에

     

    - TDD는 실패테스트 - 성공 - 리팩토링 무한반복 

    -      도메인 지식 풍부하다면

           요구사항 분석 대략적설계 객체 추출(테스트)

           UI,DB등과 의존관계 가지지않는 부분

    - 꼭 스프링이 아니더라도 자바 어플리케이션도 위와 같은 구조로 만들 수 있다.

    - 구현할 기능 목록을 작성하고 도전하는 것도 의미가 있다.

     

    2. 자동차 경주 기능 목록 및 테스트 

    > 기능목록에서 유틸성메서드,테스트 할 수 있는 메서드 뽑아내야한다.

     

    - 일단 테스트하기 쉬운 코드를 찾자 (유틸성 -  이름 split하기, 정상 이름 확인)

    - 테스트하기 어려운 것은 테스트 가능한 구조로 바꿔야함 

       오브젝트 그래프에서 다른 오브젝트에 의존관계 가지지 않는 오브젝트가 테스트 하기 쉽다.

       따라서 의존관계에 변화를 주면 테스트 가능하다.

     

     

    3. 테스트하기 어려운 테스트 해보기 (랜덤)

    public class Car {
        private static final int FORWARD_NUM =4;
        private static final int MAX_BOUND = 10;
    
        private final String name;
        private int pos = 0;
        public Car(final String name){
            if(StringUtils.isBlank(name)){
                throw new IllegalArgumentException("자동차의 이름은 존재해야함");
            }
            this.name = name.trim();
        }
    
        public int getPos() {
            return pos;
        }
    
        public String getName() {
            return name;
        }
    
        public void move(){
            if(getRandomNo() >= FORWARD_NUM)
                this.pos++;
        }
    
        private int getRandomNo() {
            Random random = new Random();
            return random.nextInt(MAX_BOUND);
        }
    
    
    }

     

    - 테스트코드 

    @Test
    void 이동(){
        Car car = new Car("pobi");
        car.move();
        assertThat(car.getPos()).isEqualTo(1);
    }
    @Test
    public void 정지(){
        Car car = new Car("pobi");
        car.move();
        assertThat(car.getPos()).isEqualTo(0);
    }

     

     

    - Car에 move가 테스트하고 싶은데 move는 random과 의존관계가 있다. -> 랜덤은 테스트가 어렵다.

    - 의존관계에 변화를 줘야한다! 

     

    1. move에 랜덤이 아닌 값을 주입하기 

    public void move(int rand){
        if(rand >= FORWARD_NUM)
            this.pos++;
    }

    - 가장 쉬운 방법이다  랜덤 값을 Car내부에서 생성하는게 아니라, RacingGame에 race()메서드에서 매개변수로 주입 받도록 변경해 주는 것이다

    - 그렇다면 Car내부에서 move매서드는 위와 같이 변할 것이고 이는 테스트하기 쉬운 메서드이다.

     

    2. 본래의 메서드 시그니처를 변경할 수 없을 때 (move를 그대로 냅둬야 할때)

     

    -> random을 발생시키는 메서드를 테스트코드에서 인스턴스 생성하며, 오버라이딩을 통해 해결하도록 한다.

     

        private int getRandomNo() {
            Random random = new Random();
            return random.nextInt(MAX_BOUND);
        }
    
    ------------------------아래로 변경------------------------
       protected int getRandomNo() {
            Random random = new Random();
            return random.nextInt(MAX_BOUND);
        }
    @Test 
    void 이동(){
     
     Car car = new Car("pobi"){
     	@Override
        protected int getRandomNo(){
        	return 4;
        }
     }
     car.move();
    assertThat(car.getPos()).isEqualTo(1);
    
    }
    
    @Test
    public void 정지(){
        Car car = new Car("pobi"){
            @Override
            protected int getRandomNo(){
                return 3;
            }
        };
        car.move();
        assertThat(car.getPos()).isEqualTo(0);
    }

     

    - 접근제어자 바꾼채로 배포해도 상관은 없다. 나중에 테스트 많이 작성되고 서비스 안정화 되면, 

       그때 다시 변경해도 된다고 한다.

     

    3. 인터페이스로 메서드 분리하기 

     

    - move에 조건들 if문에 조건에 요구사항이 자주변경된다면?

    - 로직에 해당하는 부분을 인터페이스로 분리하자 - move는 이를 외부에서 주입 받는다.

     

    - 인터페이스 

    public interface MovingStratergy {
        boolean movable();
    }

    - move메서드 변화 

    public void move(MovingStratergy movingStratergy){
        if(movingStratergy.movable())
            this.pos++;
    }

    - 테스트 

    @Test
    void 이동(){
        Car car = new Car("pobi");
        car.move(new MovingStratergy() {
            @Override
            public boolean movable() {
                return true;
            }
        });
        assertThat(car.getPos()).isEqualTo(1);
    }
    @Test
    public void 정지(){
        Car car = new Car("pobi");
        car.move(new MovingStratergy() {
            @Override
            public boolean movable() {
                return false;
            }
        });
        assertThat(car.getPos()).isEqualTo(0);
    }

    * 람다식으로 고쳐도 무방 

     

    - 프로덕션코드에 추가된  구현 클래스

    public class RandomMovingStrategy implements MovingStratergy{
        private static final int FORWARD_NUM =4;
        private static final int MAX_BOUND = 10;
        @Override
        public boolean movable() {
            return getRandomNo() >= FORWARD_NUM;
        }
        private int getRandomNo() {
            Random random = new Random();
            return random.nextInt(MAX_BOUND);
        }
    }

    - Car에서 번호 생성 및 이동 가능 여부 판단을 위 클래스로 위임하고, Car는 이를 주입받는 방식으로 변경

     

        private void moveCars() {
            //이쪽에 랜덤값을 만들어서 전달하는 방식으로 
            //오브젝트 그래프 옮기기 가능 
            for(Car car: cars){
                car.move(); 
            }
        }
        
        //RacingCar클래스 변화 
        
        private void moveCars2() {
        
        for(Car car: cars){
            car.move(new RandomMovingStrategy());
        }
    }

     

    * 이 방식은 실제 프로덕션 코드는 정상적인 구현체를 집어넣고, 테스트 시에만 빠르게 익명 인터페이스를 통해서 

      테스트해볼 수 있다는 점이다.! 

     

     

Designed by Tistory.