서론
오늘은 스프링 테스트에 대해 공부를 하였고 정리를 해보았다.
스프링 테스트
스프링 테스트란 스프링 어플레케이션을 테스트하는데 필요한 기능과 도구를 지원해주는 프레임워크로, 스프링의 다양한 구성 요소를 테스트하고 전체 애플리케이션이 올바르게 동작하는지 검증할 수 있게해준다.
🛒 스프링 테스트 의존성 추가
스프링 애플리케이션에서 스프링 테스트를 사용하기 위해 먼저 스프링 테스트 라이브러리를 추가해주어야 한다. 스프링 부트를 사용하여 프로젝트를 생성하면 다음과같이 기본적으로 스프링 테스트와 관련된 의존성이 추가 되어 있다.

❓ 테스트 라이브러리 안에는 뭐가 들어 있을까?
스프링 부트를 사용하면 기본적으로 테스트와 관련된 의존성이 자동으로 추가된다. Gradle을 통해 의존 관계를 설정하면, Maven Central Repository에서 해당 의존성을 다운로드하여 프로젝트에 추가한다. 이때 다운로드된 라이브러리에 어떤 구성 요소들이 포함되어 있는지 궁금하였다, 그래서 이 라이브러리의 내용을 확인하기 위해 다음 사이트를 접속하여 확인해보았다. Maven Central Repository

사이트에 접속하여 확인하면 엄청 많은 라이브러리가 포함되는것을 확인할 수 있다. 즉, 스프링 테스트 라이브러리를 추가하면 이처럼 다양한 라이브러리를 함께 제공하는 것을 확인할 수 있었다. 이렇게 스프링 테스트 라이브러리는 테스트에 필요한 여러 라이브러리를 통합하여, 개발자가 별도의 라이브러리를 추가하지 않아도 테스트 환경을 쉽게 구성할 수 있도록 도와주는 역할을 한다는것을 알 수 있다!
즉, spring-boot-starter-test는 스프링 부트에서 테스트를 쉽게할 수 있도록 다양한 라이브러리를 포함하는 스타터 패키지이라고 말할 수 있고, 이 패키지에 속한 다양한 라이브러리들이 통합되어, 스프링 테스트 환경이 하나의 프레임워크처럼 동작하게 된다.
📁 주요 테스트 라이브러리 간단 정리
build.gradle 파일에 스프링 테스트 의존성을 추가하면 다양한 라이브러리들이 함께 포함되는 것을 직접 확인할 수 있었다. 이제 포함된 라이브러리들 중 주요 라이브러리들의 개념을 간단하게 정리해보았고, 실습을 통해 직접 사용해보면서 사용법을 익혀볼려고 한다.
✏ JUnit
Junit은 자바 애플리케이션에서 널리 사용되는 테스트 프레임워크로, 단위 테스트 및 통합 테스트를 쉽게 작성할 수 있도록 도와주며 스프링 부트 테스트의 기본적으로 사용된다.
✏ Spring Test & Spring Boot Test
Spring Test란 스프링의 핵심 테스트 라이브러리로, 스프링 컨테이너를 로드하고, 의존성을 주입할 수 있는 기능을 제공한다. `@SpringBootTest`, `@WebMvcTest` 등의 애너테이션을 통해 스프링 환경에서 쉽게 테스트할 수 있게 도와준다.
✏ Mockito
Mockito는 가짜 객체(Mock)를 사용해 의존성을 분리하여 단위 테스트를 쉽게 할 수 있도록 도와주는 라이브러리이다. 실제 객체 대신 가짜 객체(Mock)을 만들어 테스트하려는 코드가 외부 의존성에 영향을 받지 않고 원하는 동작을 검증할 수 있도록 도와준다. `@Mock`과 `@InjectMocks` 같은 애너테이션을 사용해 객체를 간편하게 모킹할 수 있다.
✏ AssertJ
AssertJ는 테스트에서 값을 쉽게 검증할 수 있도록 도와주는 라이브러리이다. AsserJ는 직관적이고 읽기 쉬운 방식으로 결과를 확인할 수 있게 도와준다, 예를 들어, 숫자, 문자열, 리스트 등이 예상한 값과 같은지 검증할 때 사용하며, 가독성이 좋은 코드를 작성할 수 있도록 도와준다.
이 외에도 Hamcrest, JSONassert, JsonPath, JUnit Vintage 등과 같이 다양한 라이브러리를 포함하고있다. 오늘은 주로 많이 사용되는 JUnit, Spring Test & Spring Boot Test,Mockito,AssertJ 대해 실습을 하여 정리를 할 예정이다.
📚 테스트의 종류
스프링 테스트 라이브러리를 사용하기 이전에 먼저 테스트의 종류부터 이해하여야 한다, 소프트웨어 개발에서 테스트는 여러 종류로 나나뉘며, 각각의 테스트는 목적과 범위가 다르다. 오늘은 스프링 테스트에서 주로 사용하는 단위 테스트와 통합 테스트에 대해 알아보려고한다.
📝 단위 테스트(Unit Test)
단위 테스트란 소프트웨어의 개별 모듈이나 클래스의 기능을 독립적으로 테스트하는 방식을 말한다. 스프링에서 단위 테스트는 특정 클래스나 메서드가 외부 의존성 없이 독립적으로 잘 작동하는지를 검증하는 테스트 방식이라고 말할 수 있다.
예를 들어, 자동차 공장에서 여러 부품(엔진, 바퀴, 도어)이 각각 존재할 것이다. 이 부품들을 개별적으로 테스트하여 각 부품이 제 역할을 잘 수행하는지 확인하는 과정을 단위 테스트라고 말할 수 있다. 각 부품이 제데로 동작해야 나중에 이들이 모여 완성된 자동차에서도 제데로 동작할 수 있을 것이다.
이처럼 단위 테스트는 각각의 구성 요소가 올바르게 동작하는지 확인하며, 작은 단위로 테스트하기 때문에 속도가 빠르다는 장점이 있다.
📝 통합 테스트(Integration Test)
통합 테스트란 서로 다른 모듈이나 서비스가 함께 동작하는지 확인하는 테스트로, 스프링에서 실제로 어플리케이션 컨텍스트(스프링 컨테이너) 를 로드하고, 스프링 빈, 트랙잭션, 데이터베이스 연결 등의 전체 애플리케이션 흐름을 테스트하는 방식을 말한다.
예를 들어, 자동차 공장에서 여러 부품(엔진, 바퀴, 도어 등)이 각각 존재한다. 단위 테스트에서는 이 부품들을 개별적으로 테스트하여 각 부품이 제역할을 수행하는지 확인하였다. 통합 테스트는 이런 부품을 개별적으로 테스트하지 않고 하나로 조립하여 전체 시스템(자동차)이 올바르게 동작하는지 확인하는 과정을 통합 테스트라고 말할 수 있을것이다.
이처럼 통합 테스트는 각각의 구성 요소를 하나로 결합하여, 시스템이 전체적으로 올바르게 동작하는지 확인하는 과정을 말한다. 각각의 구성 요소를 하나로 결합하기 때문에 시간이 소요될 수 있지만 전체적인 시스템이 올바르게 동작하는지 확인할 수 있다는 장점이 존재한다.
🚗 단위 테스트 VS 통합 테스트 - 자동차 공장
| 특징 | 단위 테스트 | 통합 테스트 |
| 테스트 대상 | 개별 부품(엔진, 바퀴, 도어 등) | 조립된 전체 자동차 |
| 목적 | 개별 부품이 제데로 작동하는지 확인한다. | 조립된 자동차가 전체적으로 잘 동작하는지 확인한다. |
| 속도 | 빠름 | 느림 |
| 의존성 | 개별 부품에 의존한다. | 모든 부품에 의존한다. |
| 테스트 범위 | 작은 범위 | 전체 자동차 시스템 |
| 장점 | 빠른 피드백, 개별 부품의 품질 향상 | 자동차가 실제 환경에서 올바르게 동작하는지 확인이 가능하다. |
| 단점 | 부품끼리의 상호작용은 검증 불가 | 테스트 시간 소요, 복잡성 증가 |
🌱 단위 테스트 VS 통합 테스트 - 스프링
| 특징 | 단위 테스트 | 통합 테스트 |
| 테스트 대상 | 개별 클래스 / 메서드 (서비스, 리포지토리 등) | 전체 애플리케이션 흐름 |
| 목적 | 개별 클래스나 메서드가 독립적으로 잘 동작하는지 확인. | 애플리케이션의 전체 흐름과 상호작용을 검증 |
| 속도 | 빠름 | 느림 |
| 의존성 | 외부 의존성 없음 | 실제 데이터베이스, 트랜잭션, 스프링 컨테이너 |
| 테스트 범위 | 작은 단위 (클래스 또는 메서드) | 전체 애플리케이션 컨텍스트 |
| 장점 | 빠른 피드백, 개별 컴포넌트의 품질 향상 | 실제 환경에서 애플리케이션의 안정성 확인 |
| 단점 | 컴포넌트 간의 상호작용은 검증 불가 | 테스트 시간이 많이 소요, 복잡성 증가 |
🖥 주요 테스트 라이브러리 사용해보기
먼저 주요 테스트 라이브러리를 사용하여 테스트 코드를 작성하기 앞서, 간단하게 회원 정보를 CRUD할 수 있는 웹 애플리케이션을 만들었다.
JPA를 사용하여 회원 정보를 데이터베에스에서 관리하며, Member 엔티티에는 ID, 로그인아이디, 비밀번호, 이메일 필드를 포함하고 있다. 회원 정보를 CURD할 수 있는 기능을 MemberRepository 인터페이스에 정의하였고 JpaMemberRepository 가 이 인터페이스를 구현하여 데이터 베이스에 작업을 처리하도록 작성하였다.
MemberService 클래스는 비즈니스 로직을 담당하며, 컨트롤러는 REST API 형태로 회원 관련 CRUD 작업을 처리할 수 있도록 코드를 작성하였다. 전체 코드는 아래에 작성하였다. 작성한 코드를 토대롷 각 모듈별 단위 테스트 코드를 작성하고 전체적으로 통합 테스트를 하여 테스트 주요 라이브러리를 공부할 예정이다.
@Entity
@Data
public class Member {
@Id
@GeneratedValue
private Long id;
private String loginId;
private String password;
private String email;
}
public interface MemberRepository {
Member save(Member member);
Member findById(Long id);
List<Member> findAll();
Long update(Long id, MemberUpdateDto memberUpdateDto);
void delete(Long id);
}
@Repository
@RequiredArgsConstructor
@Transactional
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
@Override
public Member save(Member member) {
em.persist(member);
return member;
}
@Override
public Member findById(Long id) {
return em.find(Member.class, id);
}
@Override
public List<Member> findAll() {
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
return query.getResultList();
}
@Override
public Long update(Long id, MemberUpdateDto memberUpdateDto) {
Member member = em.find(Member.class, id);
member.setPassword(memberUpdateDto.getPassword());
member.setEmail(memberUpdateDto.getEmail());
return member.getId();
}
@Override
public void delete(Long id) {
em.createQuery("DELETE FROM Member m WHERE m.id = :id")
.setParameter("id", id)
.executeUpdate();
}
}
@Data
public class MemberUpdateDto {
private String password;
private String email;
}
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
public Member join(Member member) {
return memberRepository.save(member);
}
public Member findById(Long id) {
return memberRepository.findById(id);
}
public List<Member> findAll() {
return memberRepository.findAll();
}
public Long updateMember(Long id, MemberUpdateDto param) {
return memberRepository.update(id, param);
}
public void deleteMember(Long id) {
memberRepository.delete(id);
}
}
@RestController
@RequestMapping("/member")
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
@GetMapping
public ResponseEntity<List<Member>> listMembers() {
return new ResponseEntity<>(memberService.findAll(), HttpStatus.ACCEPTED);
}
@GetMapping("/{id}")
public ResponseEntity<Member> getMember(@PathVariable("id") Long id) {
return new ResponseEntity<>(memberService.findById(id), HttpStatus.ACCEPTED);
}
@PostMapping
public ResponseEntity<Member> join(@RequestBody Member member) {
return new ResponseEntity<>(memberService.join(member), HttpStatus.OK);
}
@PutMapping("/{id}")
public ResponseEntity<String> update(@PathVariable("id") Long id, @RequestBody MemberUpdateDto updateDto) {
memberService.updateMember(id, updateDto);
System.out.println(updateDto.getPassword());
return new ResponseEntity<>("수정 완료", HttpStatus.OK);
}
@DeleteMapping("/{id}")
public ResponseEntity<String> delete(@PathVariable("id") Long id) {
memberService.deleteMember(id);
return new ResponseEntity<>("삭제 완료", HttpStatus.OK);
}
}
🎓 Repository - Mockito 사용
Repository는 엔티티를 데이터베이스에 저장하기 위해 사용된다. 따라서 테스트 코드를 작성할 때에도 데이터를 저장하기 위해 해당 데이터베이스에 연결하고, 데이터를 저장하는데 사용하는 라이브러리에 의존해야 한다. 하지만 단위 테스트는 독립적인 환경에서 실행되어야하므로, 외부에 의존하지 않고 Repository의 동작을 검증할 수 있어야 한다.
어떻게 외부(데이터베이스)에 의존하지 않고 독립적인 환경에서 단위 테스트를 할 수 있을까? 여기서 바로 Mockito 라이브러리가 사용된다. Mockito는 가짜 객체(Mock)를 사용해 외부 의존성을 분리하고, Repository의 동작을 독립적으로 테스트할 수 있게 도와준다.
public class MemberRepositoryTest {
@Mock
MemberRepository memberRepository;
@BeforeEach
void inin() {
MockitoAnnotations.openMocks(this);
}
@Test
@DisplayName("회원가입 테스트")
void 회원가입() {
// Given
Member newMember = new Member();
newMember.setId(1L);
newMember.setLoginId("john");
newMember.setEmail("john@example.com");
newMember.setPassword("pass");
// Mocking repository
when(memberRepository.save(any(Member.class))).thenReturn(newMember);
// When
Member savedMember = memberRepository.save(new Member());
// Then
assertEquals(newMember.getLoginId(), savedMember.getLoginId());
assertEquals(newMember.getId(), savedMember.getId());
assertEquals(newMember.getPassword(), savedMember.getPassword());
assertEquals(newMember.getEmail(), savedMember.getEmail());
}
}
소스 코드에서 `MemberRepository memberRepository`위에 `@Mock` 어노테이션이 붙어있는것을 확인할 수 있다. `@Mock`은 Mokito 라이브러리에서 제공하는 어노테이션으로 해당 객체를 가짜(Mock)객체로 생성하겠다는 것을 명시하는것이다. 이렇게 생성한 `memberRepository`는 Mockito 라이브러리가 만든 가짜 객체이기 때문에 외부 의존성을 제거한 상태에서 테스트를 수행할 수 있도록 해준다.
`@Mock` 어노테이션으로 만든 가짜(Mock) 객체도 하나의 객체이므로, 이를 사용하기 전에 반드시 초기화를 해주어야 한다. 여기서는 `@BeforeEach` 어노테이션을 사용하여 모든 테스트 실행 전에 `init()` 메서드를 호출하도록 하였고 메서드 내부에 `MockitoAnnotations.openMocks(this)`를 작성하여 Mock 객체를 초기화해주었다. `this`는 해당 클래스의 인스턴스를 의미하며, 이를 통해 Mockito가 `@Mock` 어노테이션이 붙은 필드를 찾아 초기화해준다.
이제 `@Test`와 `@DisplayName` 어노테이션을 사용하여 단위 테스트를 작성하였다. `@Test` 어노테이션은 JUnit에서 테스트 메서드를 나타내며, 이를 통해 작성한 테스트가 실행되도록 한다. 또한, `@DisplayName`도 JUnit 라이브러리가 지원하는 어노테이션으로, 테스트의 이름을 커스텀하여 좀 더 직관적으로 표현할 수 있도록 도와준다.
다음으로 저장할 회원 객체를 생성한 뒤, 해당 객체의 기본적인 필드 값을 초기화해주었다. 그 다음으로, 코드 내부에서 `when(...)`을 사용하는 것을 확인할 수 있다. 이는 Mockito 라이브러리에서 stubbing 기능을 사용한것이다. Stubbing 이란 가짜로 만든 객체의 특정 메서드 호출 시 원하는 동작을 지정하는 것을 말한다. 여기서 `when(memberRepository.save(any(Member.class))).thenReturn(newMember);` 는 `memberRepository.save()` 메서드가 호출될 때 실제 데이터베이스와 상호작용 하지 않고, `newMember` 객체를 반환하도록 가짜 동작을 지정한 것이다.
다음으로 `assertEquals()` 메서드를 통해 값을 비교하고 있다. 여기서는 저장할 회원 객체와 memberRepository를 통해 저장된 객체의 필드를 비교하고 있다. `assertEquals()` 는 JUnit에서 제공하는 메서드로, 두 값이 동일한지 검증하는 역할을 한다. 테스트 중에 예상 값과 실제 값이 동일하면 테스트가 통과하고, 그렇지 않으면 테스트가 실패하게 된다. 이를 통해 특정 로직이나 메서드가 예상대로 동작하는지 확인할 수 있게 된다.

🎓 Repository - @DataJpaTest 사용
이전에는 Mockito 라이브러리를 사용하여 가짜 객체를 만들어 단위 테스트를 하였다. 이번에는 스프링 부트가 지원하는 어노테이션인 `@DataJpaTest` 어노테이션을 사용하여 단위 테스트를 작성해보려고한다.
`@DataJpaTest` 어노테이션은 스프링 부트에서 JPA 관련 컴포넌트에 대한 단위 테스트를 위해 제공하는 어노테이션이다. 이 어노테이션은 주로 데이터베이스와 관련된 Repository 레이어를 테스트 할 때 사용된다.
이 어노테이션은 Jpa 관련 설정과 함께 테스트에 필요한 설정만 구성하고 Service, Controller와 같은 Spring MVC 빈은 로드하지 않는다. 또한 JPA 기능을 테스트할 때 실제 데이터베이스에 접근하여 데이터를 처리할 수 있으며 기본적으로 테스트가 끝난후 데이터를 자동으로 롤백되게 설정되어있어, 실제 데이터베이스에 영향을 미치지않고 안전하게 테스트를 수행할 수 있다.
이전에 작성한 코드를 그대로 가져와 `@DataJpaTest`어노테이션을 사용하였다. 이렇게 설정하면 jpa와 관련된 설정과 빈만 로드될 것이다. 하지만 이렇게 테스트 코드를 작성하면 다음과 같은 오류가 발생하는것을 확인할 수 있다.
@DataJpaTest
public class DataJpaTestMemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Test
void 회원가입() {
Member member = new Member();
member.setId(1L);
member.setLoginId("test");
member.setPassword("password");
member.setEmail("test@gmail.com");
Member savedMember = memberRepository.save(member);
Assertions.assertEquals(member, savedMember);
}
}

이는 ` MemberRepository` 빈을 찾지 못해 문제가 발생한것이다. 왜 안되는지 이유를 찾아보니 이전에 MemberRepository 인터페이스를 구현한 `JpaMemberRepository`는 JpaRepository를 상속받지 않고, 직접 EntityManager를 사용해 구현되었기 때문이였다.
스프링 데이터 JPA는 `JpaRepository` 인터페이스를 구현한 인터페이스를 자동으로 구현체로 생성하고 스프링 컨테이너에 빈으로 등록하고 관리한다. 하지만 이전 코드에서는 JpaRepository를 상속받지 않고 직접구현하였기 때문에 스프링이 자동으로 해당 빈을 생성해주지 않아 테스트 코드 실행시 해당 빈을 찾을 수 없어 발생한 문제였다.
이 문제를 해결하기 위해서 `JpaRepository`를 상속받는 `JpaMemberRepository`를 인터페이스로 변경하고 JpaRepository를 상속받도록 코드를 수정하였다.
public interface JpaMemberRepository extends JpaRepository<Member, Long> {}
@DataJpaTest
public class DataJpaTestMemberRepositoryTest {
@Autowired
JpaRepository memberRepository;
@Test
void 회원가입() {
Member member = new Member();
member.setId(1L);
member.setLoginId("test");
member.setPassword("password");
member.setEmail("test@gmail.com");
Member savedMember = memberRepository.save(member);
Assertions.assertEquals(member, savedMember);
}
}

이후 테스트 코드를 실행하면 정상적으로 동작하는다는것을 확인할 수 있다. 이렇게 `@DataJpaTest` 어노테이션을 사용하면 다음과 같은 이점을 얻을 수 있다.
먼저 JPA 관련 설정과 컴포넌트만 로드하므로 불필요한 서비스나, 컨트롤러, 스프링 MVC와 관련된 빈을 로드하지 않기 때문에 테스트가 더 가볍고 빠르게 수행된다. 또한 기본적으로 H2와 같은 임베디드 데이터베이스를 사용하여 실제 데이터베이스를 대체하기 때문에 데이터베이스 의존성 없이도 빠르게 테스트할 수 있다. 또한 각 테스트가 끝난후 자동으로 롤백해주기 때문에 실제 데이터베이스에 영향을 끼치지 않고 안전하게 테스트틀 할 수 있고 실제 데이터베이스와 상호작용하며 Mock처럼 가상의 데이터가 아닌 실제 데이터가 저장되고 조회되는 과정을 테스트할 수 있게된다.
🎓 Service - Mock 사용
이제 서비스 레이어 단위 테스트를 해볼 예정이다. 서비스 레이어는 Repository나 다른 외부 서비스에 의존하는 경우가 많다. 하지만 단위테스트의 핵심 목표는 외부 의존성을 제거하고 순수한 비즈니스 로직으로 테스트하는것이 핵심이기 때문에 Mock을 사용하여 테스트 코드를 작성해였다.
@ExtendWith(MockitoExtension.class)
public class MemberServiceTest {
@Mock
MemberRepository memberRepository;
@InjectMocks
MemberService memberService;
@Test
void 회원저장() {
Member newMember = new Member();
newMember.setId(1L);
newMember.setLoginId("john");
newMember.setEmail("john@example.com");
newMember.setPassword("pass");
when(memberRepository.save(newMember)).thenReturn(newMember);
Member savedMember = memberService.join(newMember);
Assertions.assertEquals(savedMember, newMember);
}
}

여기서 `@ExtendWith(MockitoExtension.class)` 어노테이션을 확인할 수 있다. 이는 Junit5에서 지원하는 라이브러리로 Mockito와 JUnit을 통합하여 Mock 객체를 더 쉽게 사용할 수 있도록 도와준다. 이 어노테이션을 사용하면, @Mock 어노테이션으로 선언된 필드들을 자동으로 초기화할 수 있으며, 테스트 메서드 내에서 Mockito의 기능을 간편하게 사용할 수 있다. 즉, 이전에 `@BeforEach()` 를 통해 Mock 객체를 초기화 해주었는데 이렇게 안해주어도 자동으로 초기화해준다.
`MemberServic`e는 `MemberRepository`에 의존해야 한다. 따라서 `MemberRepository`를 Mock을 사용하여 가짜 객체로 생성해주었고, `@InjectMocks` 어노테이션을 사용하여 `MemberService`에 이 가짜 객체를 주입해 주었다.
@InjectMocks는 Mockito가 제공하는 어노테이션으로, Mock 객체를 테스트 대상 클래스에 자동으로 주입해 준다.이를 통해 서비스는 실제 리포지토리가 아닌, 가짜로 생성된 Mock 객체를 사용하여 테스트할 수 있게 된다.
마차간지로 Mock의 stubbing 기능인 `when(...)` 을 사용하여 가짜 객체로 생성된 memberRepository의 save 메서드의 동작을 정의하였고 객체의 값을 비교하는 방식으로 테스트 코드를 작성하였다.
🎓 Service - @DataJpaTest 사용
서비스 레이어에서 @DataJpaTest 어노테이션을 사용할 수 있는지 확인하기 위해 코드를 작성해보았다. 그러나 이 코드를 실행하면 UnsatisfiedDependencyException 예외가 발생하게 된다. 이는 @DataJpaTest가 JPA와 관련된 설정과 빈들만 로드하기 때문이다. MemberService는 서비스 계층의 클래스이므로, Service로 등록된 빈이 로드되지 않아 이러한 오류가 발생하는 것이다.
@DataJpaTest
public class MemberServiceTest {
@Autowired
JpaMemberRepository memberRepository;
@Autowired
MemberService memberService;
@DataJpaTest는 Repository와 JPA 연산에 관련된 설정만을 로드하고, 서비스 계층의 빈들은 로드하지 않기 때문에, 서비스 레이어 테스트에는 적합하지 않다.
`@DataJpaTest` 를 서비스 레이어에 사용할 수 없는줄 알았는데 `@TestConfiguration` 어노테이션을 사용하여 MemberService를 직접 등록해주는 방식으로 `@DataJpaTest`를 사용할 수 있었다. 이전 MemberService 클래스가 아닌 `JpaMemberService` JpaRepository를 주입받는 클래스를 만들어 코드를 작성하였다. 이렇게 다른 기능을 추가할 때 코드를 수정할게 많기 때문에 OCP 와 DIP 원칙을 따르지 않은 코드라고 말할수 있나유?
@DataJpaTest
@Import(MemberServiceTestConfig.class)
public class MemberServiceTest {
@Autowired
JpaMemberService memberService;
@Test
void 회원저장() {
Member newMember = new Member();
newMember.setId(1L);
newMember.setLoginId("john");
newMember.setEmail("john@example.com");
newMember.setPassword("pass");
Member savedMember = memberService.join(newMember);
Assertions.assertEquals(savedMember, newMember);
}
@TestConfiguration
static class MemberServiceTestConfig {
@Bean
public JpaMemberService memberService(JpaaMemberRepository memberRepository) {
return new JpaMemberService(memberRepository);
}
}
}
🎓 Contoller - MockMvc 사용
컨트롤러 레이어는 웹 레이어라고 불리며, 주로 HTTP 요청과 응답을 처리하는 역할을 한다. 따라서 컨트롤러 테스트에서는 실제 HTTP 요청을 모방하여, 이를 통해 컨트롤러의 동작을 검증하는 것이 중요하다. 이러한 HTTP 요청을 모방하고 컨트롤러를 테스트할 수 있도록 도와주는 라이브러리가 바로 MockMvc이다.
class MemberControllerTest {
private MockMvc mockMvc;
@Mock
private MemberService memberService;
@InjectMocks
private MemberController memberController;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(memberController).build();
}
@Test
public void 회원가입() throws Exception {
Member newMember = new Member();
newMember.setId(1L);
newMember.setLoginId("john");
newMember.setEmail("john@example.com");
newMember.setPassword("pass");
when(memberService.findById(any())).thenReturn(newMember);
mockMvc.perform(get("/member/1"))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.loginId").value("john"))
.andExpect(status().is2xxSuccessful());
verify(memberService).findById(1L);
}
}
먼저 코드에서 MockMvc를 선언한것을 확인할 수 있다. MockMvc는 컨트롤러 레이어를 테스트하기 위한 도구로 실제로 서버를 구동하지 않고 HTTP 요청과 응답을 모방하여 테스트할 수 있도록 해준다. 이를 통해 컨트롤러가 클라이언트로부터 들어오는 요청을 제대로 처리하고, 적절한 응답을 반환하는지 확인할 수 있다.
다음으로 `MockMvcBuilders.standaloneSetup(memberController).build()` 을 통하여 주어진 memberController 컨트롤러를 독립적으로 설정하도록 설정하였다. 이는 스프링 MVC 웹 애플리케이션의 다른 구성 요소들(예: 서비스, 리포지토리 등)을 로드하지 않고도 해당 컨트롤러만 테스트할 수 있게 해준다. 이제 `build()` 를 통해 설정이 완료된 MockMvc 객체를 반환한다. 반환된 MockMvc 객체를 사용하여 HTTP 요청을 시뮬레이션하고, 컨트롤러가 해당 요청에 적절히 응답하는지 테스트할 수 있게 된다.
when(...)을 통해 Mock 객체의 특정한 동작을 findById()메서드의 동작을 정의하였고 이제 mockMvc.perform을 통해 memberController 컨트롤러에 대한 HTTP 요청을 시뮬레이션한다. perform을 통해 요청을 시뮬레이션한 후, 응답 상태 코드나 응답 본문 등을 검증할 수 있게된다.
마지막으로 verify()는 Mockito 라이브러리에서 제공하는 메서드로, Mock 객체의 특정 메서드가 호출되었는지 확인하는 데 사용된다. 주로 테스트에서 예상한 동작이 실제로 발생했는지 검증하기 위해 사용한다 memberService.findById(1L)을 사용하여 findById() 메서드가 호출되었는지 확인해주었다.

🎓 Contoller - @WebMvcTest 사용
이전 방식에서는 `MockMvcBuilders.standaloneSetup()` 을 사용하여 특정 컨트롤러만 독립적으로 설정하였고, 필요한 의존 관계는 @Mock을 사용하여 Mock 객체로 주입함으로써 테스트 코드를 작성하였다.
이번에는 @WebMvcTest 어노테이션을 사용하여 테스트 코드를 작성할 예정이다.@WebMvcTest는 @DataJpaTest와 비슷하지만, 서로 다른 계층에 대한 테스트를 수행한다. @DataJpaTest는 JPA와 관련된 설정과 빈을 로드하고, @WebMvcTest는 웹 계층과 관련된 설정과 빈을 로드한다. 즉, 컨트롤러, 필터, 인터셉터 등 Spring MVC에서 웹 요청을 처리하는 데 필요한 컴포넌트만 로드하여 테스트할 수 있다.
@WebMvcTest
class MemberControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private MemberService memberService;
@Test
public void 회원가입() throws Exception {
Member newMember = new Member();
newMember.setId(1L);
newMember.setLoginId("john");
newMember.setEmail("john@example.com");
newMember.setPassword("pass");
when(memberService.findById(any())).thenReturn(newMember);
mockMvc.perform(get("/member/1"))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.loginId").value("john"))
.andExpect(status().is2xxSuccessful());
verify(memberService).findById(1L);
}
}

여기서 클래스 위에 `@WebMvcTest` 어노테이션을 선언한 것을 확인할 수 있다. 이는 컨트롤러와 관련된 빈만 로드하여 웹 계층을 테스트할 때 사용한다. 이 어노테이션은 주로 컨트롤러, 필터, 인터셉터 등 Spring MVC와 관련된 부분만 테스트하며, 서비스나 리포지토리 같은 의존성은 로드되지 않는다.
다음으로 @Autowired 를 통해 MockMvc를 자동으로 주입받아, 이 객체를 사용해 컨트롤러에 대한 HTTP 요청을 시뮬레이션하도록 하였다.
또한, @MockBean을 사용하여 MemberService는 실제 구현체가 아닌 Mock 객체로 대체하였다. 테스트에서는MemberService의 메서드 호출을 Mock으로 처리하며, 이를 통해 컨트롤러가 실제 서비스 계층에 의존하지 않도록 설정하였다.
결론
오늘은 스프링 테스트에 대해 공부하였고 다양한 테스트 방법을 실습해보았다. 스프링 테스트는 스프링 애플리케이션에서 다양한 구성요소를 검증하고 애플리케이션이 의도대로 동작하는지 확인하는 중요한 도구이며, 스프링 테스트 환경을 구성하는데 필요한 라이브러리도 알아보았따.
또한 테스트의 종류로 단위 테스트, 통합 테스트를 구분하여 공부해보았고 주요 라이브러리도 사용할 수 있었다.. 통합테스트는 내일 해봐야겠다...
'TIL' 카테고리의 다른 글
| [TIL - 2024-09-19]트랜잭션 정리 (0) | 2024.09.19 |
|---|---|
| [TIL - 2024-09-17] JDBC, DriverManager, HikariCp, DataSource (0) | 2024.09.16 |
| [TIL - 2024-09-11] 싱글톤 패턴, 스프링 빈 생명주기 콜백, 스프링 빈 스코프, DL, Proxy (0) | 2024.09.11 |
| [TIL - 2024-09-10] 제어의 역전, 의존성 주입, 스프링 컨테이너 (0) | 2024.09.10 |
| [TIL - 2024-09-10] 좋은 객체 지향 설계의 5가지 원칙 SOLID (1) | 2024.09.10 |
