[테스트] - Persistence Layer에서의 단위 테스트 잘하는 방법

yeom yaloo·2024년 3월 24일
0

백엔드 관련 지식

목록 보기
4/7

🧺 Persistence Layer?

영속성을 부여해주는 Persistence Layer 계층

1. 영속성이란?

  • 데이터를 생성한 프로그램이 종료되어도 사라지지 않는데이터를 영속성이라고 해요.
  • 영속성이 없으면 데이터는 메모리에서만 존재하고 프로그램이 종료되면 해당 데이터는 모두 사라지게 돼요.
  • 그래서 우리는 프로그램이 종료되어도 데이터가 휘발되지 않게 하기 위해서 데이터베이스에 영구 저장해 데이터에 영속성을 부여해요.

2. Persistence Layer를 위한 기술(Persistence Framework)

  • 대표적으로 우리가 사용할 JPA(즉, ORM) 기술이 이에 해당해요.
    • 데이터베이스 테이블을 객체처럼 사용할 수 있다는 장점이 있어요.
    • 또한 객체 간의 관계를 바탕으로 SQL을 자동 생성해주어 따로 SQL문을 작성해줄 필요가 없고 객체 모델과 관계형 모델 간의 불일치 문제를 해결해줘요.
  • SQL Mapper 역시 해당 계층을 위해 사용되는 Persistence Framework에 해당해요.
    • 객체와 SQL의 필드를 맵핑하여 데이터를 객체화하는 기술이에요.
    • SQL문을 직접 작성하고 쿼리 수행 결과를 어떤 객체에 매핑하여 줄지 직접 바인딩 할수 있는 방법이에요.
    • 이는 SQL에 의존적이고 개발자가 SQL을 하나 하나 작성해야 한다는 단점이 있어요.
    • JDBC, Mybatise가 이에 해당해요.

🚲 Persistence Layer Test?

💡 우리는 이제 간단하게 해당 계층에 대한 테스트 작업을 진행할 거예요.

그 전에 JPA를 사용하면 사용할 수 있는 테스트 작업과 슬라이싱 테스트라는 것을 나눠 알아볼 것인데! 이를 어떻게 사용할 수 있는지를 잘 보고 실제 기능 테스트에 적용해주세요!

JPA Repository 테스트 코드 구조

1. 예시 레포지토리 코드 - JPA를 이용한 레포지토리 만들기

public interface OrederRepository extends JpaRepository<Oreder, Long> {

}
  • JPA가 제공하는 레포지토리 형식이며 인터페이스에 해당 JpaRepository 인터페이스를 상속 받아 사용하는 구조예요.
    • 이때 JpaRepository<Entity, Entity PK Type> 형식으로 넣어주셔야 해요.

2. 예시 테스트 코드 - @DataJpaTest

package com.example.springbootjpatest.repository;

@DataJpaTest
public class OrderRepositoryTests {

}
  • @DataJpaTest 해당 애너테이션은 JPA가 제공하는 레포지토리를 사용할 경우에 사용이 가능한데요
  • 이를 사용하면 우리는 따로 Rollback 작업과 관련해서 설정을 하지 않아도 된다는 장점이 있어요.
  • 그래서 우리는 JPA를 사용한 레포지토리에 클래스 레벨에 해당 애너테이션을 달아주면 편하게 테스트가 가능해집니다.

3. 패키지 구조

자바 코드 작성 패키지

Untitled

  • 여기까지가 일반적으로 작업을 진행하는 자바 코드 진영이에요.

테스트 코드 작성 패키

Untitled

JPA Repository Test Code

1. User Entity

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String email;

    // Getters and setters

2. UserRepository with JPA

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {

}
  • JPA를 사용한 Repository 작성

3. DummyUser

public class DummyUser {

    public static User createDummyUser(String username, String email) {
        User user = new User();
        user.setUsername(username);
        user.setEmail(email);
        return user;
    }
}
  • 해당 User 객체를 더미로 만드는 이유는 간단해요. 재 사용성이 좋기 때문이에요.
  • 왜냐하면 User 객체를 만들어서 테스트를 여러 곳에서 진행한다면 매번 해당 User 객체를 생성해주는 것보단 이 DummyUser 객체를 반환하는 메서드를 만들어두고 객체 생성 없이 static 메서드로 바로 사용이 가능하는 것이 더 높은 재사용성을 보장해요.
    • User user = DummyUser.createDummyUser("testuser", "test@example.com"); 이런 식으로 말이죠!
  • 이는 선택적인 사항으로 굳이 이렇게 진행하지 않아도 되지만 고려해볼 법 하답니다.

4. UserRepositoryTest

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

@DataJpaTest
public class UserRepositoryTests {

    @Autowired
    private UserRepository userRepository;

    private User user1;
    private User user2;

    @BeforeEach
    public void setUp() {
        user1 = DummyUser.createDummyUser("user1", "user1@example.com");
        user2 = DummyUser.createDummyUser("user2", "user2@example.com");
        userRepository.save(user1);
        userRepository.save(user2);
    }

		@DisplayName("회원 저장 테스트")
    @Test
    public void testSaveUser() {
		    //given
        User user = DummyUser.createDummyUser("testuser", "test@example.com");
        
        //when
        userRepository.save(user);

				//then
        assertNotNull(user.getId()); // ID가 생성되었는지 확인
    }

    @Test
    public void testFindAllUsers() {
        List<User> users = userRepository.findAll();
        assertEquals(2, users.size()); // 저장된 사용자 수가 맞는지 확인
    }
}
  • DataJpaTest
    • JPA를 이용한 레포지토리를 테스트할 때 사용해요. 이때 이 작업은 통합 테스트(= @SpringBootTest를 이용한 작업이 아님)가 아니기 때문에 더 가볍고 저렴하게 테스트가 가능해요.
    • 또한 이 애너테이션은 데이터베이스와 관련된 빈들만 로드되기 때문에 테스트가 빠르게 수행되고 테스트 환경이 가볍다는 장점이 있어요.
  • @BeforeEach
    • @Test 애너테이션이 붙은 메서드들의 실행 전에 작동해요. 이 작업은 선행 작업이라고 생각하면 돼요.
  • @DisplayName
    • 해당 애너테이션을 사용하면 테스트의 이름을 부여할 수 있어요. 기존 Junit4에서는 이를 위해서 테스트명을 한글로 했지만 이는 추천되지 않습니다.
    • 대분의 자바 코드는 영어로 작성하는 것이 통상적으로 사용되는 방법이기 때문이죠. 그래서 Junit5에서는 해당 애너테이션이 등장했고 이를 사용해서 테스트 작업 후에도 해당 테스트 이름을 보고 쉽게 이 내용이 무엇인지 파악할 수 있게 했어요.
    • 추가로 Junit5가 테스트 이름을 지정하게 하는 방법을 알고 싶다면?

Untitled


🎪 테스트 구조에 관해서…

테스트 권장 구조


@DataJpaTest
public class UserRepositoryTests {

    //생략

    @Test
    public void testSaveUser() {
		    //given
        User user = DummyUser.createDummyUser("testuser", "test@example.com");
        
        //when
        userRepository.save(user);
				
				
				//then
        assertNotNull(user.getId()); // ID가 생성되었는지 확인
    }

}
  • 기본적으로 테스트는 given , when , then 으로 이루어져 있어요.
  • given
    • 테스트의 시작 지점이에요.
    • 테스트 수행에 필요한 초기 상태나 조건을 설정해요.
    • 주어진 상황에서는 테스트 수행을 위해 필요 데이터를 설정하거나 객체를 초기화 하는 작업을 이곳에서 진행해요.
  • when
    • 테스트 하려는 동작을 수행하는 단계예요.
    • 특정 행동 또는 메서드를 호출해요.
    • 보통은 주어진 상황에 대해서 특정한 동작 수행이 있을 때 결과를 검증해요.
  • 절대적으로 해당 구조로 진행되는 것은 아니예요. 그러나! 이런 구조를 이용해서 테스트를 진행하는 것이 더 테스트를 이해하기 쉽고 의도를 명확하게 전달할 수 있게 해주기 때문에 권장하고 있어요.

🔪 슬라이스 테스트

슬라이스 테스트?

1. 정의

  • 말 그대로 레이어별로 잘라서, 레이어를 하나의 단위로 보는 단위 테스트를 한다는 것이 슬라이스 테스트예요.

2. Spring.io

Test slicing is about segmenting the ApplicationContext that is created for your test. Typically, if you want to test a controller using MockMvc, surely you don’t want to bother with the data layer. Instead you’d probably want to mock the service that your controller uses and validate that all the web-related interaction works as expected.

테스트 슬라이싱(Test slicing)은 테스트할 때 생성되는 ApplicationContext를 세분화하는 것입니다. 일반적으로 MockMvc를 사용하여 컨트롤러를 테스트하려는 경우 데이터 레이어에 신경 쓸 필요가 없습니다. 대신에 컨트롤러가 사용하는 서비스를 모킹하고, 모든 웹 관련 상호 작용이 예상대로 작동하는지 확인할 것입니다.

즉, 테스트 슬라이싱은 테스트를 세분화하여 필요한 부분만 포함하도록 합니다. 데이터 레이어, 서비스 레이어, 웹 레이어 등과 같은 다양한 레이어를 포함할 수 있습니다. 예를 들어, 컨트롤러를 테스트할 때 실제 데이터베이스에 액세스할 필요 없이 서비스 레이어만 모킹하여 웹 상호 작용을 테스트할 수 있습니다.

이를 통해 테스트를 더욱 효율적으로 작성하고, 필요한 부분만 집중적으로 테스트할 수 있습니다. 결과적으로 테스트 코드의 실행 속도가 향상되고 테스트 관리가 더욱 용이해집니다.

3. 왜 슬라이스 테스트를 할까?

@SpringBootTest 어노테이션을 이용하면 모든 테스트를 할 수 있는데 왜 레이어별로 잘라서 테스트할까?

@SpringBootTest 어노테이션의 단점은 아래와 같아요.

  • 실제 구동되는 애플리케이션의 설정, 모든 Bean을 로드하기 때문에 시간이 오래걸리고 무거워요.
  • 테스트 단위가 크기 때문에 디버깅이 어려운 편이에요.
  • 결과적으로 웹을 실행시키지 않고 테스트 코드를 통해 빠른 피드백을 받을 수 있다는 장점이 희석돼요.

따라서 @SpringBootTest 어노테이션은 어플리케이션 컨텍스트 전체를 사용하는 통합 테스트에 사용돼야 합니다.

슬라이스 테스트에 사용할 수 있는 어노테이션 종류

스프링 부트는 자동 설정의 일부만을 테스트 슬라이스로 가져와서 테스트에 활용할 수 있도록 다양한 테스트 어노테이션을 제공해줘요.

아래는 대표적인 슬라이스 테스트 어노테이션입니다.

  • @WebMvcTest
  • @WebFluxTest
  • @DataJpaTest
  • @JsonTest
  • @RestClientTest

레포지토리 구조

1. 인터페이스

public interface QueryRoleRepository {

	Optional<Role> findById(Long id);

}

2. 구현체

@RequiredArgsConstructor
@Repository
public class QueryRoleRepositoryImpl implements QueryRoleRepository {

	private final JPAQueryFactory jpaQueryFactory;

	@Override
	public Optional<Role> findById(Long id) {
		QRole role = QRole.role;

		return Optional.ofNullable(
			jpaQueryFactory
				.selectFrom(role)
				.where(role.roleId.eq(id))
				.fetchOne()
		);
	}
}
  • 해당 레포지토리를 QueryDsl을 이용한 레포지토리예요.
  • JPA를 사용하지 않은 것을 확인할 수 있죠?
  • 기본적으로 이런 일반 레포지토리를 @DataJpaTest를 이용해서 테스트하기 위해선 슬라이스 테스트를 진행해야 해요.
    • 그렇지 않으면 JPA를 사용하지 않은 레포지토리이기 때문에 빈 주입 문제가 생겨요!

슬라이스 테스트 설정 파일 적용 전 테스트 코드

package com.fisa.infra.role.repository.querydsl;

import static org.assertj.core.api.Assertions.*;

import java.util.Optional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.fisa.infra.role.domain.entity.Role;
import com.fisa.infra.role.repository.querydsl.inter.QueryRoleRepository;

import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;

@SpringBootTest
@Transactional
class RoleRepositoryTest {

	@Autowired
	EntityManager entityManager;

	@Autowired
	QueryRoleRepository roleRepository;
	private Role user;
	private Role admin;

	@BeforeEach
	void setUp() {
		admin=Role.createRole("ROLE_ADMIN");
		user=Role.createRole("ROLE_USER");
	}

	@Test
	@DisplayName("id값으로 Role 찾아오는 테스트")
	void findById() {

		entityManager.persist(admin);
		entityManager.persist(user);

		Optional<Role> opAdmin = roleRepository.findById(1L);
		Optional<Role> opUser = roleRepository.findById(2L);

		assertThat(opAdmin.isEmpty()).isFalse();
		assertThat(opUser.isEmpty()).isFalse();

		assertThat(opAdmin.get().getName()).isEqualTo("ROLE_ADMIN");
		assertThat(opUser.get().getName()).isEqualTo("ROLE_USER");
	}
}
  • @SpringBootTest
    • 해당 애너테이션을 사용함에 따라서 전체 컨텍스트가 스캔되는 비효율이 발생해요! 전체적인 컨텍스트의 스캔이 있기 때문에 비용과 시간이 많이 들겠죠?
  • @Transactional
    • @DataJpaTest와 달리 해당 애너테이션을 달아주어야 데이터베이스 내에 데이터 변경이 없어져요. 해당 애너테이션이 처리해주던 롤백 문제까지 우리가 신경 써야 한다는 의미니 귀찮은 일이 많아졌겠죠?
  • 그래서 우리는 이 비용과 시간 절감 그리고 데이터 변경 문제까지 한 번에 처리해주고 있는 @DataJpaTest 사용을 위해서 아래와 같이 테스트 설정 파일을 수정하고 사용합니다.

테스트 설정 파일 및 테스트 코드 작성 방법

1. 첫 번째 방법

설정 파일 건들이기

package com.fisa.infra.config;

@TestConfiguration
public class QuerydslTestConfig {

	@PersistenceContext
	private EntityManager entityManager;

	@Bean
	public JPAQueryFactory jpaQueryFactory(){
		return new JPAQueryFactory(this.entityManager);

	}

}
  • @TestConfiguration
    • 테스트 환경에서의 설정 파일임을 알리는 애너테이션이에요.
  • 위의 작업은 QueryDsl 설정 파일과 별반 다름이 없이 적용해줘요.
  • 테스트 작업을 위한 설정 파일은 테스트 디렉토리 하위에 놓아주셔야 합니다.
  • 해당 작업에서는 따로 repository에 대한 빈 등록이 없기 때문에 그 작업을 따로 테스트 코드 내에서 진행해야 합니다.

테스트 코드 작성하기

package com.fisa.infra.account.repository.querydsl;

import java.util.Optional;

import static org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.NONE;

@AutoConfigureTestDatabase(replace = NONE)
@Import(QuerydslTestConfig.class)
@DataJpaTest
class QueryAccountRepositoryImplTest {

    private QueryAccountRepository testQueryAccountRepository;

    @Autowired
    private JPAQueryFactory jpaQueryFactory;

    private Account account;

    @BeforeEach
    void setUp() {
    	//given
        account = DummyAccount.dummy();
        //여기서 이제 실제 구현체를 생성해줘서 해당 작업을 진행하는 방법
        testQueryAccountRepository = new QueryAccountRepositoryImpl(jpaQueryFactory);
    }

    @Test
    void queryFindAccountByLoginId() {
        //given

        //when
        Optional<AccountDTO> op = testQueryAccountRepository.queryFindAccountByLoginId("test");

        Assertions.assertThat(op.isEmpty()).isFalse();

    }

}
  • 해당 방법은 아예 테스트 설정 파일 안에서 레포지토리를 @Bean으로 만들어주는 방법입니다.
  • 또한 해당 configuration 파일을 사용하기 위해서는 @Import(TestConfig.class)를 이용해주어야 합니다.

2. 두 번째 방법

설정 파일 건들이기

package com.fisa.infra.config;

@TestConfiguration
public class QuerydslTestConfig {

	@PersistenceContext
	private EntityManager entityManager;
    
	 @Bean
	 public QueryAccountRepositoryImpl testQueryAccountRepository() {
	 	return new QueryAccountRepositoryImpl(new JPAQueryFactory(entityManager));
	 }

}

테스트 코드 작성하기

package com.fisa.infra.account.repository.querydsl;

@AutoConfigureTestDatabase(replace = NONE)
@Import(QuerydslTestConfig.class)
@DataJpaTest
class QueryAccountRepositoryImplTest {
	
	@Autowired
    private QueryAccountRepository testQueryAccountRepository;

    private Account account;

    @BeforeEach
    void setUp() {
        account = DummyAccount.dummy();
    }

    @Test
    void queryFindAccountByLoginId() {
        //given

        //when
        Optional<AccountDTO> op = testQueryAccountRepository.queryFindAccountByLoginId("test");
				
				
				//then
        Assertions.assertThat(op.isEmpty()).isFalse();

    }

}
profile
즐겁고 괴로운 개발😎

0개의 댓글