728x90
Controller - Service - Repository - Domain 계층의 테스트하는 방식이 각각 다르다.



- Controller, Service, Repository는 스프링이 관리하는 bean으로 @SpringBootTest 어노테이션을 붙여서, Domain 은 스프링이 관리하는 bean이 아니므로 단위 테스트(클래스 테스트)를 하면 된다.
- Controller 같은 경우는 응답받는 JSON을 비롯한 HTTP 위주의 테스트를 하면 된다(응답 코드, 반환 JSON...)
- Service, Repository는 데이터 위주의 테스트를 진행하면 된다.
UserService.createRequest() Test
@SpringBootTest //스프링 부트 테스트(빈으로 등록된 객체를 테스트해야되므로)
class UserServiceTest @Autowired constructor(
/*
@Autowired private val userRepository: UserRepository,
@Autowired private val userService: UserService,
*/
private val userRepository: UserRepository,
private val userService: UserService,
) {
@Test
fun saveUserTest() {
//given
val request = UserCreateRequest("최태현", null)
//when
userService.saveUser(request)
//then
//실제 DB에 값이 있는지 확인
val results = userRepository.findAll()
assertThat(results).hasSize(1)
assertThat(results[0].name).isEqualTo("최태현")
assertThat(results[0].age).isNull() //코틀린은 age가 null일지 아닐지를 모르기 때문에 null이 아니라고 단언한다(코틀린 플렛폼)
//null을 허용하기 위해서는 @nullable 어노테이션을 붙여야 한다.
}
}
User.class
@Entity
public class User {
//...
@NotNull
public String getName() {
return name;
}
@Nullable
public Integer getAge() {
return age;
}
//....
}
- @SpringBootTest : 스프링 컨텍스트를 띄우는 테스트임을 표시하며, 이 테스트가 실행될 때는 컨텍스트가 자동으로 뜬다.
- 생성자에서 @Autowired 어노테이션을 통해 bean을 주입받는다.
- 이때, 매번 @Autowired 가 중복이 되므로, @Autowired construtor를 통해 중복을 제거할 수 있다
- saveUserTest에서는 UserCreateRequest를 생성하여 name = "최태현", age = null을 넣었다.
- 자바에선 age는 Integer 타입으로 null을 넣을 수 있다
- saveUserTest를 실제 실행하면 오류가 발생한다
- kotlin에서 Integer타입을 age가 null일지 아닐지를 판단할 수 없기 때문에 기본적으로는 null이 아니라고 판단하게 되는데 이를 코틀린 플랫폼 타입이라고 한다.
- 현재는 null이 아닌 변수에 age를 담으려고 하니 오류가 발생하는 것이다(results [0]. age에서 getter를 호출하는 데 이 값이 null이기 때문)
- 이를 해결하기 위해서는 null이 불가능한 타입에는 @NotNull 어노테이션을 null이 가능한 타입에는 @Nullable 어노테이션을 붙이면 된다.
UserService.saveUser, getUsers, updateUser, deleteUser Test
@AfterEach
fun clear() {
userRepository.deleteAll()
}
@Test
@DisplayName("유저 저장이 정상 동작한다.")
fun saveUserTest() {
//given
val request = UserCreateRequest("최태현", null)
//when
userService.saveUser(request)
//then
//실제 DB에 값이 있는지 확인
val results = userRepository.findAll()
assertThat(results).hasSize(1)
assertThat(results[0].name).isEqualTo("최태현")
assertThat(results[0].age).isNull() //코틀린은 age가 null일지 아닐지를 모르기 때문에 null이 아니라고 단언한다(코틀린 플렛폼)
//null을 허용하기 위해서는 @nullable 어노테이션을 붙여야 한다.
}
@Test
@DisplayName("유저 조회가 정상 동작한다.")
fun getUsersTest() {
//given
userRepository.saveAll(
listOf(
User("A", 20),
User("B", null)
)
)
//when
val results = userService.getUsers()
//then
//단위 실행하면 오류가 발생하지 않지만, 모두 실행하면 SpringContext를 공유하기 떄문에 문제가 된다.
//이를 해결하기 위해서는 @AfterEach에서 database를 초기화 해주자
assertThat(results).hasSize(2)
assertThat(results).extracting("name").containsExactlyInAnyOrder("A", "B") //["A", "B"]
assertThat(results).extracting("age").containsExactlyInAnyOrder(20, null) //[20, null]
}
@Test
@DisplayName("유저 업데이트가 정상 동작한다.")
fun updateUserNameTest() {
//given
val saveUser = userRepository.save(User("A", null))
val request = UserUpdateRequest(saveUser.id, "B")
//when
userService.updateUserName(request)
//then
val result = userRepository.findAll()[0]
assertThat(result.name).isEqualTo("B")
}
@Test
@DisplayName("유저 삭제가 정상 동작한다.")
fun deleteUserTest() {
//given
userRepository.save(User("A", null))
//when
userService.deleteUser("A")
//then
assertThat(userRepository.findAll()).isEmpty()
}
- getUsers() 테스트에서는 extracting("프로퍼티").containsExactlyInAnyOrder()를 사용해서 포함 여부를 체크한다
- 전체 실행하면, 동작하지 않는다 -> SpringContext를 공유하기 때문
- @SpringBootTest를 실행하면 스프링 컨텍스트에서 공유하고 있는 DB가 같기 때문에, 저장 테스트에서 사용되었던 데이터가 조회 테스트에서 조회가 되어 문제가 발생한다.
- 이를 해결하기 위해서는 테스트를 진행한 뒤에는 반드시 DB를 초기화해야 한다.
- 메서드마다 deleteAll() 메서드를 호출해 줄 수 도 있지만, afterEach에서 자동화시키는 것이 좋다
- 테스트 메서드의 이름을 직관적인 한글 이름을 붙이고 싶다면, @DisplayName 어노테이션을 붙이면 된다.
Book 관련 기능 TEST
@SpringBootTest
class BookServiceTest @Autowired constructor(
private val bookService: BookService,
private val bookRepository: BookRepository, //마지막에 콤마를 찍을 수 있다 -> git diff에 변하는 부분만 보기 위해 해당 기능 허용
private val userRepository: UserRepository,
private val userLoanHistoryRepository: UserLoanHistoryRepository,
) {
@AfterEach
fun clean() {
bookRepository.deleteAll()
userRepository.deleteAll()
}
@Test
@DisplayName("책 등록이 정상 동작한다")
fun saveBookTest() {
//given
val request = BookRequest("이상한 나라의 엘리스")
//when
bookService.saveBook(request)
//then
val books = bookRepository.findAll()
assertThat(books).hasSize(1)
assertThat(books[0].name).isEqualTo("이상한 나라의 엘리스")
}
@Test
@DisplayName("책 대출이 정상 동작한다")
fun loanBookTest() {
//given
bookRepository.save(Book("이상한 나라의 엘리스"))
val savedUser = userRepository.save(User("최태현", null))
val request = BookLoanRequest("최태현", "이상한 나라의 엘리스")
//when
bookService.loanBook(request)
//then
val results = userLoanHistoryRepository.findAll()
assertThat(results).hasSize(1)
assertThat(results[0].bookName).isEqualTo("이상한 나라의 엘리스")
assertThat(results[0].user.id).isEqualTo(savedUser.id)
assertThat(results[0].isReturn).isFalse
}
@Test
@DisplayName("책이 진작 대출되어 있다면, 신규 대출이 실패한다")
fun loanBookFailTest() {
//given
bookRepository.save(Book("이상한 나라의 엘리스"))
val savedUser = userRepository.save(User("최태현", null))
userLoanHistoryRepository.save(UserLoanHistory(savedUser, "이상한 나라의 엘리스", false)) //대출 중인 상태
val request = BookLoanRequest("최태현", "이상한 나라의 엘리스")
//when & then
assertThrows<IllegalArgumentException> {
bookService.loanBook(request)
}.apply {
assertThat(message).isEqualTo("진작 대출되어 있는 책입니다")
}
}
@Test
@DisplayName("책 반납이 정상 동작한다")
fun returnBookTest() {
//given
val savedUser = userRepository.save(User("최태현", null))
userLoanHistoryRepository.save(UserLoanHistory(savedUser, "이상한 나라의 엘리스", false)) //대출 중인 상태
val request = BookReturnRequest("최태현", "이상한 나라의 엘리스")
//when
bookService.returnBook(request)
//then
val results = userLoanHistoryRepository.findAll()
assertThat(results).hasSize(1)
assertThat(results[0].isReturn).isTrue
}
}
UserLoanHistory.class
@Entity
public class UserLoanHistory {
//...
@NotNull
public String getBookName() {
return this.bookName;
}
@NotNull
public User getUser() {
return user;
}
@NotNull
public boolean isReturn() {
return isReturn;
}
}
- 책을 이름과 유저 이름, 반환 여부는 null이 될 수 없으므로 @NotNull 어노테이션을 붙인다.
- 책 대출 테스트 같은 경우 정상적으로 대출이 되었을 경우와, 이미 책을 빌려 정상 동작하지 않는 경우를 테스트해야 한다.
- assertThrows를 활용해 IllegalargumentException 이 잘 발생하는지 확인한다.
728x90
'실전! 코틀린과 스프링 부트로 도서관리 애플리케이션 개발하기' 카테고리의 다른 글
| Ch02. Java 서버를 Kotlin 서버로 리팩토링하자 - Kotlin과 JPA를 함께 사용할 때 주의할 점 (0) | 2022.11.01 |
|---|---|
| Ch02. Java 서버를 Kotlin 서버로 리팩토링하자 - Domain 계층 리펙토링하기 (0) | 2022.11.01 |
| Ch01. 도서관리 애플리케이션 리팩토링 준비하기 - JUnit5 사용하기 (0) | 2022.10.29 |
| Ch01. 도서관리 애플리케이션 리팩토링 준비하기 - 수동 테스트 코드 작성하기 (0) | 2022.10.29 |
| Ch01. 도서관리 애플리케이션 리팩토링 준비하기 - 테스트 코드?! (0) | 2022.10.29 |