목차

테스트가 어려운 코드

❏ 하드 코딩된 경로

...
Path path = Paths.get("D:\\data\\pay\\cpoo1.csv");
..

   - 완전하게 일치하는 경로에 파일이 없으면 테스트 불가능, 맥OS 나 리눅스 를 사용하는 개발자 역시 테스트 불가능

 

테스트를 어렵게 하는 하드코딩 예 : 파일의 경로, IP 주소, 포트 번호

 의존 객체를 직접 생성

public class PaySync {
  //의존 대상을 직접 생성                 
  private PayInfoDao payInfoDao = new PayInfoDao();

  public void sync() throws IOException{
    ...
    payInfos.forEach(pi -> payInfoDao.insert(pi);
  }
}

 

이 코드를 테스트하려면 PayInfoDao가 올바르게 동작하는데 필요한 모든 환경을 구성해야 한다.

DB를 준비해야 하고 필요한 테이블도 만들어야 한다

 

테스트를 실행하면 데이터가 DB에 추가되므로 같은 테스트를 다시 실행하기 전에 기존에 들어간 데이터를 삭제해야 한다.

그렇지 않으면 중복 데이터로 인해 데이터 갑입에 실패하게 된다.

 정적 메서드 사용

클래스의 인스턴스가 아니라 클래스에 직접 연결되어 있기 때문에, 일반적인 의존성 주입이나 모킹이 어렵다.

 

테스트할 때 특정 메서드의 행동을 모의로 대체하거나 변경하고 싶다면,

    인스턴스 메서드의 경우 해당 객체를 모킹가능,

    정적 메서드의 경우 그럴 수 없어 실제 메서드를 호출

이로 인해 테스트가 복잡해지고, 테스트 환경과 실제 환경 사이의 격리가 어려워진다.

 

 가능하다면 정적 메서드 대신 인스턴스 메서드를 사용, 필요한 의존성을 생성자나 세터를 통해 주입하는 것이 좋다.

 실행 시점에 따라 달라지는 결과

현재 시간이나 날짜를 기준으로 사용자의 구독상태를 계산하는 경우

Random 을 이용해서 임의 값을 사용하는 코드

→ 시점에 따라 테스트 결과가 달라지면 믿을 수 없는 테스트 코드가 된다.

 역할이 섞여 있는 코드

여러 역할이 섞여 있는 코드는 특정 기능만 테스트하기가 쉽지 않다.

 그 외 테스트가 어려운 코드

  • 메서드 중간에 소켓 통신 코드가 포함되어 있다.
  • 콘솔에서 입력을 받거나 결과를 콘솔에 출력한다.
  • 테스트 대상이 사용하는 의존 대상 클래스나 메서드가 final 이다. 이 경우 대역으로 대체가 어려울 수 있다.
  • 테스트 대상의 소스를 소유하고 있지 않아 수정이 어렵다.

테스트 가능한 설계

앞에서 살펴본 테스트가 어려운 주된 이유는 의존하는 코드를 교체할 수 있는 수단이 없기 때문

 하드 코딩된 상수를 생성자나 메서드 파라미터로 받기

생성자나 세터를 이용해서 경로를 전달 받기

메서드를 실행할 때 인자로 전달받기

 의존 대상을 주입 받기

생성자를 통해서 의존 대상을 주입하게 수정

 테스트하고 싶은 코드를 분리하기

역할이 섞여 있는 코드에서는 테스트하고 싶은 코드를 별도 기능으로 분리해서 테스트를 진행

 시간이나 임의 값 생성 기능 분리하기

테스트 대상이 사용하는 시간이나 임의 값을 제공하는 기능을 별도로 분리해서 대역으로 처리

 외부 라이브러리는 직접 사용하지 말고 감싸서 사용하기

외부 라이브러리와 연동하기 위한 객체를 따로 만들고, 테스트 대상을 분리한 타입을 사용하게 변경

 

변경전

public class LoginService {
    private String authKey = "somekey";
    private CustomerRepository customerRepo;

    public LoginService(CustomerRepository customerRepo) {
        this.customerRepo = customerRepo;
    }

    public LoginResult login(String id, String pw) {
        int resp = 0;
        boolean authorized = AuthUtil.authorize(authKey);
        if (authorized) {
            resp = AuthUtil.authenticate(id, pw);
        } else {
            resp = -1;
        }
        if (resp == -1) return LoginResult.badAuthKey();

        if (resp == 1) {
            Customer c = customerRepo.findOne(id);
            return LoginResult.authenticated(c);
        } else {
            return LoginResult.fail(resp);
        }
    }

}

 

 

변경 후 코드

public class AuthService {
    private String authKey = "somekey";

    public int authenticate(String id, String pw) {
        boolean authorized = AuthUtil.authorize(authKey);
        if (authorized) {
            return AuthUtil.authenticate(id, pw);
        } else {
            return -1;
        }
    }
}

public class LoginService {
    private AuthService authService = new AuthService();
    private CustomerRepository customerRepo;

    public LoginService(CustomerRepository customerRepo) {
        this.customerRepo = customerRepo;
    }

    public void setAuthService(AuthService authService) {
        this.authService = authService;
    }

    public LoginResult login(String id, String pw) {
        int resp = authService.authenticate(id, pw);
        if (resp == -1) return LoginResult.badAuthKey();

        if (resp == 1) {
            Customer c = customerRepo.findOne(id);
            return LoginResult.authenticated(c);
        } else {
            return LoginResult.fail(resp);
        }
    }
}

 

의존하는 대상이 final 클래스거나 의존 대상의 호출 메서드가 final인 경우에도 동일한 기법으로 테스트 가능

 

+ Recent posts