강의를 수강하게된 계기
링크드인에서 이동욱 인프런 CTO님을 팔로워했는데, Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트 강의에대한 평가를 너무 좋게 작성해서 놀랬다는 글을 보았다.
nextStep에서 객체지향적으로 설계하는 방법과 TDD 작성을 배웠음에도 실무에 적용하려고 했을때, 유동적이지 못했던 경험이 있어서 좀 더 공부하고 싶은 마음에 강의를 수강하게 되었다.
5월 12일 현재 섹션 1까지 들었는데, 나에게 제일 인상 깊었던 부분과 실무에서도 적용하면 좋을 것 같다고 생각드는 부분에 작성하도록 하겠다.
테스트에서의 제일 중요한 부분
1.
제일 중요한 건 당연하게도 소형 테스트
소형 테스트를 진행하기 위해서는 행위보다는 상태를 테스트 하는게 좋다.
테스트에서 사용하는 개념
•
SUT : System Under test(테스트 하려는 대상)
@Test
void 유저테스트() {
//given
User sut = User.builder()
.bookmark(new ArrayList<>())
.build(); // 테스트 하려는 대상
}
Java
복사
•
테스트 픽처 : 테스트에 필요한 자원을 생성하는 것
private User sut;
@BeforeEach
void 사용자는_미리_할당합니다() {
sut = User.builder()
.bookmark(new ArrayList<>())
.build(); // 테스트 하려는 대상
}
Java
복사
•
더미(dummy) : 아무런 동작도 하지 않고, 정상적으로 돌아가기 위해 전달하는 객체
@Test
public void 이메일_회원가입을_할_수있다() {
//given
UserCreateRquest userCreateReuqest = userCreateRequest.builder()
.email("foo@localhost.com")
.password("123456")
.build();
UserService sut = UserService.builder()
.registerEmailSender(new DummyRegisterEmailSender())
.userRepository(userRepository)
.build();
sut.register(userCreateReuqest)
}
//더미 객체가 되는 것.
class DummyRegisterEmailSender implements RegisterEmailSender {
@Override
public void send(String email, String message) {
}
}
Java
복사
•
속임(Fake) : Local에서 사용하거나 테스트에서 사용하기 위해 만들어진 가짜 객체
@Test
public void 이메일_회원가입을_할_수있다() {
//given
UserCreateRquest userCreateReuqest = userCreateRequest.builder()
.email("foo@localhost.com")
.password("123456")
.build();
UserService sut = UserService.builder()
.registerEmailSender(new DummyRegisterEmailSender())
.userRepository(userRepository)
.build();
sut.register(userCreateReuqest)
}
//더미 객체가 되는 것.
class FakeRegisterEmailSender implements RegisterEmailSender {
private final Map<String, List<String>> latestMessages = new HashMap<>();
@Override
public void send(String email, String message) {
List<String> records = latestMessage.getOrDefault(email, new ArrayList<>());
records.add(message);
latestMessages.put(email, records);
}
public Optional<String> findLatestMessage(String email) {
return latestMessages.getOrDefault(email, new ArrayList<>()).stream.findFirst();
}
}
Java
복사
•
stub : 미리 준비된 값을 출력하는 객체
class StubUserRepository implements UserRepository {
public User getByEmail(String email) {
if (email.equals("foo@bar.com")) {
return User.builder()
.email("foo@bar.com")
.status("PENDING")
.build();
}
throw new UsernameNotFoundException(email);
}
}
Java
복사
의존성 ️️️️️️
강의를 들으면서 정말 좋았던 부분이였다. 의존성 주입과, 의존성 역전을 통해서 유연하게 테스트를 진행 할 수 있고, 소스코드 관리도 더 좋게 할 수 있다는 부분이였다. 이부분은 소스코드를 남기고, 차후에도 볼 수 있도록 정리 하였다.
•
의존성 주입 (DI) : 인스턴스를 직접 만드는 것 이 아니라, 상위 에서 생성해서 생성자를 통해 객체를 매개변수로 받는다.
•
의존성 역전 (DIP) : 인터페이스난 추상 클래스 같은 추상적인 선언을 참조해서 사용
️️️ 테스트 잘하는 방법
테스트를 잘 하려면 의존성 주입과 의존성 역전을 잘 다를 수 있어야 한다.
user 객체 마지막 로그인 시간 테스트
•
user 객체
class User {
private long lastLoginTimestamp;
public void login() {
//..
this.lastLoginTimestamp = Clock.systemUTC().millis();
}
}
Java
복사
◦
내부 로직은 Clock에 의존함.
◦
외부에서 보면 login이 시간에 의존하고 있는지 알 수 없다.
문제발생
•
User의 로그인 시간을 테스트 하기 위해서는 어떻게 해야되는가?
class UserTest {
@Test
public boid login_테스트() {
User user = new User();
user.login();
assertThat(user.getLastLoginTimestamp()).isEqualTo(???); // 😭🥳 어떻게 테스트 해야 될 지 모른다.
}
}
Java
복사
•
내부에서 의존적으로 코드를 작성 했기 때문에 테스트 진행이 안된다.
해결방법
•
의존성 주입으로 해결 가능함.
class User {
private long lastLoginTimestamp;
public void login(Clock clock) {
//..
this.lastLoginTimestamp = clock.millis();
}
}
Java
복사
•
User 테스트 코드
class UserTest {
@Test
public boid login_테스트() {
User user = new User();
Clock clock = Clock.fixed(Instant.parse("2000-01-01T00:00:00.00Z"));
user.login(clock);
assertThat(user.getLastLoginTimestamp()).isEqualTo(946684800000L);
}
}
Java
복사
UserService 테스트
•
UserService 클래스
class UserService {
public void login(User user) {
user.login(Clock.systemUTC());
}
}
Java
복사
문제발생 ️️️
•
userServiceTest
class UserServiceTest {
@Test
public void login_테스트() {
User user = new User();
userService userService = new UserService();
userService.login(user);
assertThat(user.getLastLoginTimestamp()).isEqualTo(???);
}
}
Java
복사
•
유저 서비스에서 테스트를 진행하려고 할 때 의존성 주입으로 고정값을 지정해줘야된다. → 결국에는 어디선가 User에서 처럼 내부의 값을 숨겼기 때문에 테스트하기 어려워진다. 이를 해결 하기위해서는 DIP 의존성 역전을 사용해야된다.
해결방법️️️
•
Clock interface
interface ClockHolder {
long getMillis();
}
Java
복사
•
TestClockHolder
@AllArgsConstructor
class TestClockHolder implements ClockHolder {
private Clock clock;
public long getMillis() {
return Clock.millis();
}
}
Java
복사
•
userService
@Service
@RequiredArgsConstructor
class UserService {
private final ClockHolder clockHolder;
public void login(User user) {
user.login(clockHolder)
}
}
Java
복사
•
UserServiceTest
class UserServiceTest {
@Test
public void 로그인_테스트() {
Clock clock = Clock.fixed(Instant.parse("2000-01-01T00:00:00.00Z"));
User user = new User();
UserService = new UserService(new TestClockHolder(clock));
//when
userService.login();
//then
assertThat(user.getLastLoginTimestamp()).isEqualTo(946684800000L);
}
}
Java
복사
◦
항상 일관된 테스트 코드를 작성 할 수 있고, 쉽게 깨지지 않는다.
이런 형식으로 Controller도 DIP를 사용해서 테스트를 진행한다.
신기하게도 기존에 [만들면서 배우는 클린 아키텍처]의 핵사고날 아키텍처가 되기 시작하고, 기존에 읽었던 책들을 다시한번 생각하는 계기가 되었다.
후기
오늘은 [Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트] 라는 강의를 보았다. 자바를 시작하거나, 스프링을 시작한 지 얼마 안 됐다면, 어려울 수도 있겠지만 현업에서 Spring을 하고 있는 사람이라면 충분히 이해할 것이라고 생각되고, 테스트하는데 큰 도움이 된 강의였다.
핵심은 DI와 DIP를 이용하여, Spring, Mock, DB에 의존적이지 않게 테스트하고, 단위 테스트를 잘해야 된다는 것 같다. 정말 많은 도움이 된 강의입니다.! 추천드립니다.