2jigoo / BookStudy-StartTdd

'테스트 주도 개발 시작하기' 스터디
2 stars 0 forks source link

[5주차] 테스트 가능한 설계 - jongha #23

Closed Jonghai closed 1 year ago

Jonghai commented 1 year ago

[5주차] 테스트 가능한 설계

Chapter 8. 테스트 가능한 설계

스터디 일시 2023.09.08

목표

Jonghai commented 1 year ago

테스트 가능한 설계

테스트가 어려운 코드

하드 코드된 경로

//파일 경로가 하드코딩되어 있는 테스트 대상
Path path = Paths.get("D:\\data\\pay\\cp0001.csv");

의존객체를 직접 생성

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

정적 메서드 사용

    boolean authorized = AuthUtil.authorized(authKey);
    if (authorized){
        resp = AuthUtil.authenticate(id,pw);
    }else {
        resp = -1;
    }

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

예) 사용자의 포인트를 계산하는 로직

public class UserPointCalculator {
    private PointRule pointRule = new PointRule();
    private SubscriptionDao subscriptionDao;;
    private ProductDao productDao;

    public UserPointCalculator(SubscriptionDao subscriptionDao, ProductDao productDao){
        this.subscriptionDao = subscriptionDao;
        this.productDao = productDao;
    }
public int calculatePoint(User u){
    Subscription s = subscriptionDao.selectByUser(u.getId());
    if (s == null) throw new NoSuchFieldException();
    Product p = productDao.selectById(s.getProductId());
    LocalDate now = LocalDate.now();
    int point = 0;
    if (s.isFinished(now)){
        point += p.getDefaultPoint();
    }else {
        point += p.getDefaultPoint();
    }
    if (s.getGrade() == GOLD){
        point += 100;
    }
    return point;
    }
}

역할이 섞여 있는 코드

테스트 가능한 설계

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

public void setFilePath(String filePath){
    this.filePath = filePath;
}

public void sync() throws IOExecption{
    Path path = Paths.get(filePath);
}

세터 메서드를 추가하여 파일 경로를 변경해서 테스트 할 수 있다.

의존 대상을 주입 받기

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

예) 사용자의 포인트를 계산하는 로직

계산 기능만 테스트하려면

포인트 계산 코드만 테스트하고 싶다면 해당 코드를 별도 기능으로 분리해서 테스트를 진행.

public class PointRule {
    public int calculatePoint(Subscription s, Product p, LocalDate now){
        int point = 0;
        if (s.isFinished(now)){
            point += p.getDefaultPoint();
        }else {
            point += p.getDefaultPoint() + 10;
        }
        if (s.getGrade() == GOLD){
            point += 100;
        }
        return point;
    }
}
    @Test
    void 만료전_GOLD등급은_130포인트() {
        PointRule rule = new PointRule();
        Subscription s = new Subscription(
                LocalDate.of(2019,5,5),
                Grade.GOLD);
        Product p = new Product();
        p.setDefaultPoint(20);
       int point = rule.calculate(s, LocalDate.of(2019,5,1));
       assertEquals(130,point);
    }

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

현재 일자를 구하는 기능을 분리하고 분리한 대상을 주입할 수 있게 변경

public class Times{

    pulbic LocalDate toDay(){
        return LocalDate.now();
    }
}
public class DailyBatchLoader {
    private String basePath = ".";
    private Times times = new Times();

    public void setTimes(Times times){
        this.times = times;
    }

    public int load(){
        LocalDate date = times.today();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
        Path batchPath = Paths.get(basePath,date.format(formatter), "batch.txt");
        //batchPath에서 데이터를 읽어와 저장하는 코드
        return result;
    }
}

시간을 구하는 기능을 별도로 분리하면 테스트를 하기 수월해진다.

}


### 외부 라이브러리는 직접 사용하지 말고 감싸서 사용하기
- 외부 라이브러리가 정적 메서드를 제공한다면 대체할 수 없다.

*예) AuthUtil 클래스*
- `AuthUtil.authorize()`와 `AuthUtil.authenticate()` 메서드는 정적 메서드이기 때문에 대역으로 대체가 어려움.

대역으로 대체하기 어려운 외부 라이브러리가 있다면
- 외부 라이브러리와 연동하기 위한 타입으로 따로 만든다.
- 테스트 대상은 분리한 타입을 사용하게 바꾼다.

테스트 대상 코드는 새로 분리한 타입을 사용함으로써 외부 연동이 필요한 기능을 대역으로 대체할 수 있다.

*외부 라이브러리를 감싼 클래스*
```java
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);
        }
    }
}

AuthService를 대역으로 대체하여 인증 성공 상황과 실패 상황에 대해 LoginService가 올바르게 동작하는지 검증하는 테스트 코드를 만들 수 있다.