
이 시리즈는 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 메소드로 생성해서 주입하는 건 사실상 하드 코딩이기 때문입니다DI와 DIP는 완전히 다른 개념입니다DIP
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);
input을 변경하고, output을 쉽게 검증할 수 있는가?Clock의 경우처럼 호출하는 입장에서는 알 수 없는 경우입니다output이 테스트하기 어려운 상황이라고 합니다public class Example {
public void processData(int[] numbers) {
int sum = 0;
for (int number : numbers) {
sum += number;
}
System.out.println("Sum: " + sum);
}
}
private 메서드는 테스트하지 말아라private 메서드를 테스트해야 한다면, 그건 설계의 문제라고 테스트가 알려주는 것이니 public으로 분리해내야 합니다final 메소드를 stub 해야하는 상황이 생긴다면, 설계가 잘못된 상황입니다final이라는 키워드는 더이상 변경하지 않겠다는 뜻이므로, 이 부분을 stub 한다면 무언가 잘못된 설계를 뜻합니다DRY(Don't Repeat Yourself): 반복하지 않기DAMP(Descriptive And Meaningful Phrase): 서술적이고 의미 있는 문구DRY가 더 중요하겠지만 테스트에서는 반복되더라도 의미를 확실하게 알 수 있는 DAMP가 우선되어야 한다는 뜻입니다