이 시리즈는 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
가 우선되어야 한다는 뜻입니다