Java/Test
JUnit을 사용하여 예제로 TDD 맛보기
keep it simple
2023. 8. 2. 16:29
TDD(Test Driven Development)란?
- 테스트 중심적 개발이다. -> 테스트를 중심으로 개발하는 개발 방법론이다. 당연히 TDD는 설계 테스트 등 복잡하지만 이글에서는 간단하게 TDD관련된 메인 아이디어만 맛보기로 예시와 작성한 글이다.
- TDD 작성 단계
- RED: 테스트를 먼저 작성한다. 구현이 없으므로 테스트는 실패 해야한다.
- GREEN: 테스트를 성공시키기 위해 최소 요구조건에 맞게 코드 품질 상관없이 구현한다.
- REFACTOR: 테스트 코드를 성공하면서 개선하고 리팩토링한다.
Advantages:
- 협력 & 문서 -> 많은 개발자들이 협력해도 테스트 코드가 작성이 잘 되어있으면 파악이 쉬워 개발 생산성이 향상된다.
- 좋은 설계 -> 테스트 코드를 짜면서 코드를 분리하고 리팩토링 함으로써, 결합도를 낮게함으로써 좋은 설계를 생각하게 된다.
- 유지보수의 용이 -> 테스트 코드를 한번만 작성하면 코드가 리팩토링되도 쉽게 테스트 후 유지보수가 가능하다.
- 좋은 퀄리티의 코드 -> 높은 코드 품질을 보장한다. 테스트가 먼저 작성되서 안정성과 코드 품질이 좋아진다.
Disadvantages:
- 테스트를 가능하도록 설계 -> 추상화, 의존성 주입, 설계 등 테스트를 하기위해 고려해야하는 부분이 많다.
- 개발시간 증가 -> 고민할 부분이 많아 개발시간이 증가되며 오래걸린다.
Example
- 유저가 회원가입하는 서비스와 테스트 코드를 작성해보자.
- 요구조건:
- 나이가 15살 이상
- 중복 닉네임 불가
- 요구조건:
Domain Layer
@Entity
@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int age;
private String name;
private String nickname;
private LocalDateTime joinedAt;
@Builder
private User(int age, String name, String nickname) {
this.age = age;
this.name = name;
this.nickname = nickname;
}
public static User create(int age, String name, String nickname) {
return User.builder()
.age(age)
.name(name)
.nickname(nickname)
.build();
}
public Boolean isAgeUnder15(int age){
return age < 15;
}
}
public interface UserRepository extends JpaRepository<User,Long> {
Optional<User> findByNickname(String nickname);
}
Application Layer
@Service
@RequiredArgsConstructor
public class OrderService {
private final UserRepository userRepository;
@Transactional
public UserResponse createOrder(OrderCreateRequest request, LocalDateTime registeredDateTime) {
return UserResponse.of(savedUser);
}
}
User Request
@Getter
@NoArgsConstructor
public class UserJoinRequest {
private int age;
private String name;
private String nickname;
}
User Response
@Getter
public class UserResponse {
private final Long id;
private final int age;
private final String name;
private final String nickname;
private final LocalDateTime joinedAt;
@Builder
private UserResponse(Long id,int age,String name,String nickname,LocalDateTime joinedAt){
this.id = id;
this.age = age;
this.name = name;
this.nickname = nickname;
this.joinedAt = joinedAt;
}
public static UserResponse of(User user){
return UserResponse.builder()
.id(user.getId())
.age(user.getAge())
.name(user.getName())
.nickname(user.getNickname())
.joinedAt(user.getJoinedAt())
.build();
}
}
TDD STEPS
Red Phase:
- Service
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional
public UserResponse joinUser(UserJoinRequest request) {
return null;
}
}
- Test
@Test
@DisplayName("유저가 회원가입을 한다.")
void joinUserTest(){
// given
UserJoinRequest request = UserJoinRequest.builder()
.age(15)
.name("john")
.nickname("test")
.build();
// when
UserResponse response = userService.joinUser(request);
// then
assertThat(response).isNotNull();
}
- Result
Green Phase:
- Service
@Transactional
public UserResponse joinUser(UserJoinRequest request) {
User user = User.create(request.getAge(), request.getName(), request.getNickname());
userRepository.findByNickname(request.getNickname())
.ifPresent(i -> {
throw new IllegalStateException("이미 존재하는 닉네임입니다.");
});
if (user.isAgeUnder15(request.getAge())) {
throw new IllegalArgumentException("나이가 15살 이상이어야합니다.");
}
User savedUser = userRepository.save(user);
return UserResponse.of(savedUser);
}
- Test
@Test
@DisplayName("유저가 회원가입을 한다.")
void joinUserTest(){
// given
UserJoinRequest request = UserJoinRequest.builder()
.age(15)
.name("john")
.nickname("test")
.build();
// when
UserResponse response = userService.joinUser(request);
// then
assertThat(response).isNotNull();
assertThat(response).extracting("age","nickname","name")
.contains(15,"john","test");
}
- Result
Refactoring Phase:
- Refactored Service: 한 함수에 있던 로직들을 역할때 따라 메소드를 분리하여 리팩토링 했다.
- 데이터 검증 메소드, request -> domain 메소드 , 나이 15살 이상인 지 확인하는 메소드
@Transactional
public UserResponse joinUser(UserJoinRequest request) {
validateUserRequest(request);
User user = createUserFromRequest(request);
User savedUser = userRepository.save(user);
return UserResponse.of(savedUser);
}
private void validateUserRequest(UserJoinRequest request) {
userRepository.findByNickname(request.getNickname())
.ifPresent(i -> {
throw new IllegalStateException("이미 존재하는 닉네임입니다.");
});
if (userIsUnder15(request.getAge())) {
throw new IllegalArgumentException("나이가 15살 이상이어야합니다.");
}
}
private User createUserFromRequest(UserJoinRequest request) {
return User.create(request.getAge(), request.getName(), request.getNickname());
}
private boolean userIsUnder15(int age) {
return age < 15;
}
- Result
결론
- 확실히 위 스텝처럼 테스트 코드를 짜면 계속 생각하게 되고 중간에 부족한 점이라던지 수정해야할 부분을 파악하게 된다. 더군다나 마지막 리팩토링 스텝을 통해 더욱 더 가독성있고 깔끔하게 코드를 짤 수 있다. (보통은 예외처리와 도메인 단위테스트도 같이 진행해야하지만 코드가 길어지면 글읽는 피로감 때문에 유저 회원가입 테스트만 작성했다.)
- TDD는 Test Code를 더욱 더 짜임새있고 생각하며 짤 수 있게 도와주는 개발론이라 생각한다.그리고 TDD는 필수가아니지만 Test Code는 필수라 생각한다. 초반 개발만 봤을때는 개발시간이 증가된다 생각하지만 전체적으로 봤을 땐테스트코드 없이 개발 후 운영상태에서 발견되는 버그 수정을 생각하면 오히려 개발시간이 감소되고 더 안전하고 확장성있게 개발할 수 있다고 생각한다. 또한, 개발은 혼자아는게 아니라 여럿이서 협력하고 더 좋은 프로덕트를 만들기 위해서 테스트 코드를 작성하면서 많은 생각과 서로간의 아이디어와 고민하고 이야기해야 더 나은 협력하고 좋은 품질의 코드를 짜는 개발자가 된다 생각한다. 이제까지는 선택적으로 테스트코드를 짯지만 이제 개발할 때는 필수로 작성해야겠다 생각한다.