의존성과 Testability

Gyeongjae Ham·2023년 6월 19일
0

TEST

목록 보기
3/7
post-thumbnail

이 시리즈는 TDD를 숙달하기 전에 TEST 자체에 대한 이해를 높이기 위한 학습 시리즈입니다

의존성

  • 결합과 같은 개념입니다, 다른 객체의 함수를 사용하는 상태를 뜻합니다

의존성 주입

  • DI(Dependency Injection): 의존성을 약화시키는 기술을 말합니다
class Chef {
	pulbic Hamburger makeHamburger() {
    	Bread bread = new Bread();
        Meat meat = new Meat();
        Lettuce lettuce = new Lettuce();
        Source source = new Source();
        
        return Hamburger.builder()
        		.bread(bread)
                .meat(meat)
                .lettuce(lettuce)
                .source(source)
                .build();
}
  • Chef 클래스는 Bread, Meat, Lettuce, Source에 의존하고 있는 상태입니다
  • 이 상태에서 new 메서드로 생성해서 주입하는게 아닌 외부로부터 주입받는 걸 의존성 주입이라고 말하고 아래와 같은 형태로 변합니다
class Chef {
	pulbic Hamburger makeHamburger(
        	Bread bread,
	        Meat meat,
	        Lettuce lettuce,
	        Source source
		) {        
        return Hamburger.builder()
        		.bread(bread)
                .meat(meat)
                .lettuce(lettuce)
                .source(source)
                .build();
}
  • DI는 의존성을 약화시키는 것일뿐 의존성을 완전히 없앤 상태는 아닙니다
  • 인스턴스를 만드는 것보다 의존성 주입을 받는게 좋은 이유는 new 메소드로 생성해서 주입하는 건 사실상 하드 코딩이기 때문입니다

의존성 역전

  • DIDIP는 완전히 다른 개념입니다
  • DIP
    1. 상위 모듈은 하위 모듈에 의존하면 안되고, 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다
    2. 추상화는 세부 사항에 의존해서는 안됩니다. 세부사항이 추상화에 의존해야 합니다

  • 인터페이스나 추상 클래스같은 정책을 바라보게 하고, 구현체(세부사항)는 정책을 바라보게 만들어야 합니다

의존성과 테스트

  • 테스트를 잘 구성하려면 의존성 주입과 의존성 역전을 잘 다룰 수 있어야 합니다
  • 의존성이 숨겨져 있는 경우에는 테스트하기 좋지 않은 코드이고, 이것은 테스트가 우리에게 보내는 신호입니다
class User {
	private long lastLoginTimestamp;
    
    public void login() {
    	// ...
        this.lastLoginTimestamp = Clock.systemUTC().mills();
  • 위 클래스는 외부에서 login() 메서드를 사용할 때, 내부적으로 Clock에 의존되어 있다는 사실이 숨겨져있습니다. 때문에 테스트를 작성하려고 해도 아래와 같은 문제가 발생합니다
class UserTest {
	@Test
    public void login_테스트() {
    	// given
        User user = new User();
        
        // when
        user.login();
        
        // then
        assertThat(user.getLastLoginTimestamp()).isEqualTo(?????);
  • 로그인한 시간을 비교해야 하는데 외부에서는 어떤 값을 비교해야 하는지 전혀 알 수가 없게 됩니다

  • 의존성 주입을 통한 해결
class User {
	private long lastLoginTimestamp;
    
    public void login(Clock clock) {
    	// ...
        this.lastLoginTimestamp = clock.mills();
    }
}
  • 위와 같이 외부에서 Clock을 주입받는 형태로 리팩토링을 했습니다. 그럼 의존성 주입으로 어떤 테스트가 가능해졌는지 확인해 보도록 하죠
class UserTest {
	@Test
    public void login_테스트() {
    	// given
        User user = new User();
        Clock clock = Clock.fixed(Instant.parse("2000-01-01T00:00:00.00Z"), ZoneId.of("UTC"))
        
        // when
        user.login(clock);
        
        // then
   		assertThat(user.getLastLoginTimestamp()).isEqualTo(946684800000L);
  • 하지만 위 예제처럼 코드를 변경한다고 해도, 결국 더 상위 모듈에서 Clock을 주입해줘야 하는 시점은 생기고 하드 코딩을 또 하게 될 겁니다
  • 그 상황에서 바로 의존성 역전을 통해서 풀어내게 되는 겁니다
interface ClockHolder {
	long getMills();
}
  • ClockHolder라는 인터페이스를 만들고, User 클래스가 해당 주입받게 한다면 더 상위 모듈인 UserService에서 User 클래스를 사용할 때, 인터페이스를 구현한 구현체 중 아무거나 주입해서 이용할 수 있게 됩니다
@Service
@RequiredArgsConstructor
class UserService {
	private final ClockHolder clockHolder;
    
    public void login(User user) {
    	// ...
        user.login(clockHolder);
    }
}
  • 상위 모듈인 UserService에서 login() 메서드를 테스트하는 코드는 이런식으로 구현될 수 있겠습니다
class UserTest {
	@Test
    public void login_테스트() {
    	// given
        User user = new User();
        Clock clock = Clock.fixed(Instant.parse("2000-01-01T00:00:00.00Z"), ZoneId.of("UTC"))
        UserService userService = new UserService(new TestClockHolder(clock));
        
        // when
        userService.login(clock);
        
        // then
   		assertThat(user.getLastLoginTimestamp()).isEqualTo(946684800000L);

Testability

  • 얼마나 쉽게 input을 변경하고, output을 쉽게 검증할 수 있는가?

input이 테스트하기 어려운 상황

  1. 호출자는 모르는 입력이 존재하는 경우
    • 위에서 살펴봤던 Clock의 경우처럼 호출하는 입장에서는 알 수 없는 경우입니다
  2. 하드 코딩이 된 경우
    1. 파일 경로 등 값이 하드 코딩이 된 경우
    2. 외부 시스템이 하드 코딩이 된 경우

output이 테스트하기 어려운 상황

  • 아래 예시처럼 결과를 확인할 길이 없는 경우에 output이 테스트하기 어려운 상황이라고 합니다
public class Example {
	public void processData(int[] numbers) {
    	int sum = 0;
        for (int number : numbers) {
        	sum += number;
        }
        System.out.println("Sum: " + sum);
    }
}

테스트 작성에 대한 기타 조언

  1. private 메서드는 테스트하지 말아라
    • private 메서드를 테스트해야 한다면, 그건 설계의 문제라고 테스트가 알려주는 것이니 public으로 분리해내야 합니다
  2. final 메소드를 stub 해야하는 상황이 생긴다면, 설계가 잘못된 상황입니다
    • 애초에 final이라는 키워드는 더이상 변경하지 않겠다는 뜻이므로, 이 부분을 stub 한다면 무언가 잘못된 설계를 뜻합니다
  3. DRY < DAMP
    • DRY(Don't Repeat Yourself): 반복하지 않기
    • DAMP(Descriptive And Meaningful Phrase): 서술적이고 의미 있는 문구
    • 비지니스 로직 등 부분에서는 DRY가 더 중요하겠지만 테스트에서는 반복되더라도 의미를 확실하게 알 수 있는 DAMP가 우선되어야 한다는 뜻입니다
  4. 논리 로직을 피하라
    • 테스트에서는 if, for, 덧셈, 뺄셈 등을 넣지 말자라는 뜻입니다
profile
Always be happy 😀

0개의 댓글