[JUnit] JPA + Mocking 테스트

merci·2023년 5월 18일
1

Spring

목록 보기
19/21

테스트의 목적은?

코드 수정이 발생했을때 화면의 절차대로 하지 않겠다. -> 빠르게 수정 및 해결
다른 코드에 영향이 가는지 체크할 수 있다.

배포 시 발생할 문제를 미리 파악하기 위해 통합 테스트를 이용한다.


@SpringBootTest

SpringBootTest는 통합테스트시에 사용되며 실제 서버가 운영될때 필요한 빈들이 모두 로드되어 테스트된다.
AOP, 필터, 컨트롤러, 서비스, 레파지토리, 데이터소스(EntityManager) 등등 필요한 모든것들이 메모리에 생성된다.
스프링 컨텍스트를 로드한다고 표현한다.

따라서 일반적으로는 DataJpaTest를 이용한 서비스로직 테스트와 WebMvcTest를 이용한 단위테스트를 한 뒤 SpringBootTest를 이용해서 통합테스트를 진행한다.


@WebMvcTest

WebMvcTest는 말 그대로 웹 요청을 단위테스트하기 위해 필요한 빈들만 로드되어 테스트한다.
로드되는 빈들은 필터, 디스패처 서블릿, 컨트롤러까지 로드되고 디스패처 서블릿이 로드되기 때문에 핸들러, 인터셉터, 뷰리졸버, 메세지컨버터등도 함께 로드된다.
로드되지 않는 나머지 빈들은 Mock객체로 대체되어 테스트의 격리를 보장한다.


@DataJpaTest

  • Jpa테스트에 필요한 빈들만 로드하므로 테스트를 간단하고 빠르게 할 수 있다.
  • 기본적으로 인 메모리 DB를 사용하여 테스트를 진행하고 테스트 완료시 메모리를 날린다.
  • 각 테스트가 독자적으로 진행되도록 설정 되어 있다. 테스트 종료시 트랜잭션이 자동으로 롤백되므로 다른 테스트에 영향이 없다.
  • 기본적으로 @Entity를 스캔하여 사용하므로 별도의 설정이 필요 없다.
  • JpaRepository 또한 자동적으로 스캔하므로 @Autowired로 주입해서 사용할 수 있다.
  • @ActiveProfiles("test") 와 함께 사용해 test 프로파일 설정으로 테스트를 할 수 있다.
  • @Sql은 테스트 전후에 실행 될 SQL 스크립트를 설정한다.
@DataJpaTest
public class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    public void testSaveUser() {
        User user = new User("username", "password");
        userRepository.save(user);
        User retrievedUser = userRepository.findByUsername("username");
        assertEquals(user.getUsername(), retrievedUser.getUsername());
    }
}

AOP 테스트

만약에 AOP를 적용시켰을때 WebMvcTest를 진행하려는 경우 테스트가 잘 되지 않을 수 있다.
이럴때는 AOP를 비활성해서 단위테스트를 진행하는 방법이 있고 Mockito같은 라이브러리를 이용하면 AOP가 적용된 Advice를 mock객체로 대체하여 테스트 할수도 있다.
또는 AOP를 우회하는 방법도 있는데 이번 테스트에서는 AOP프록시를 생성하여 테스트하도록 한다.

@EnableAspectJAutoProxy // AOP 작동 활성화
@Import(MyValidAdvice.class) // Aspect 클래스 로드
@WebMvcTest(UserController.class) 
public class UserControllerTest {
	// 생략
}

AOP를 @Aspect로 구현하면 스프링 실행시 AOP프록시가 비즈니스로직을 감싸게 되는데 동일한 환경을 만들기 위해 @EnableAspectJAutoProxy를 이용하여 프록시 객체를 생성하여 AOP를 활성화 한다.
@Import로 AOP가 발생시킨 익셉션을 핸들링하는 Aspect 클래스를 컨텍스트에 로드할 수 있다.

@SQL

테스트 케이스를 실행하기 전이나 실행한 후에 SQL 스크립트를 실행할 수 있게 해준다.
테스트에 필요한 데이터를 사전에 준비할수 있고 테스트완료 후 데이터를 원상태로 돌릴수도 있다.
특정 테스트에만 @SQL를 사용하여 특정한 데이터를 이용한 테스트를 할 수도 있다.

@Sql("classpath:db/teardown.sql")
public class UserControllerTest{ ... }

테스트 전에 teardown.sql을 실행한다.
제약조건을 제거해야 원할하게 데이터를 삭제할 수 있다.

-- 모든 제약 조건 비활성화
SET REFERENTIAL_INTEGRITY FALSE;
truncate table user_tb;
SET REFERENTIAL_INTEGRITY TRUE;
-- 모든 제약 조건 활성화

@ActiveProfiles("test")

스프링부트의 컨벤션에 따라 application-<profile>.yml의 프로파일 설정을 이용한다.

@ActiveProfiles("test")
@Sql("classpath:db/teardown.sql")
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class UserControllerTest extends MyRestDoc {

@AutoConfigureMockMvc

테스트를 위한 MockMvc 인스턴스를 스프링부트가 자동으로 구성해서 컨텍스트에 추가한다.
하지만 MockMvc는 디스패처서블릿 이전의 테스트는 할 수 없다. ( 필터, 서블릿 컨테이너 )
주로 통합테스트시에 사용된다.

@WithUserDetails

Spring Security Test에서 제공하는 어노테이션으로 @WithUserDetails 어노테이션을 사용하면 테스트를 실행하기 전에 지정된 사용자 이름으로 UserDetailsService를 통해 사용자 정보를 조회 후 조회된 사용자 세부 정보를 SecurityContext에 설정한다.
따라서 주어진 사용자명으로 인증된 것처럼 테스트를 실행할 수 있게 해준다.

	// BeforeEach 실행전 인증 세션을 생성한다.
    @WithUserDetails(value = "ssar", setupBefore = TestExecutionEvent.TEST_EXECUTION)
    // BeforeEach 실행후 인증 세션을 생성한다.
    @WithUserDetails(value = "ssar", setupBefore = TestExecutionEvent.TEST_METHOD)
    @Test
    public void saveAccount_test() throws Exception {



모킹(Mocking)이란 ?

테스트 시에 컴포넌트의 의존성을 mock객체로 대체하는것을 말한다.
이러한 의존성을 통제해 일관적이고 독립적인 테스트를 할 수 있다.
발생 가능한 예외를 예상하여 테스트할 수 있다.

이러한 모킹은 Mockito라이브러리를 추가해 사용할수 있다.
기본적으로 제공하는 spring-boot-starter-test 의존성에서도 Mockito가 포함되어 있어 추가하지 않아도 된다.

dependencies {
	testImplementation 'org.mockito:mockito-core:5.3.1'  
    // 포함되어 있음
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

mock객체를 정의하는 방법

import static org.mockito.Mockito.*;

// 모의 객체 생성
List<String> mockedList = mock(List.class);

// 모의 객체에 대한 동작 정의
when(mockedList.get(0)).thenReturn("첫 번째 요소");

// 테스트 수행
System.out.println(mockedList.get(0));  // "첫 번째 요소" 출력

MockBean을 이용해 가짜 userRepository를 만들어서 테스트한다.
이를 통해 실제 DB에는 접근하지 않고 서비스의 동작만을 테스트할 수 있다.
실제 DB에 접근 하지 않기 때문에 mockUser를 만들어서 모킹을 한다.

@DataJpaTest
@ExtendWith({SpringExtension.class, MockitoExtension.class})
public class UserServiceTest {

    @MockBean
    private UserRepository userRepository;

    @Autowired
    private UserService userService;

    @Test
    public void testGetUserByName() {
        User mockUser = new User();
        mockUser.setName("John");

        Mockito.when(userRepository.findByName("John"))
            .thenReturn(Optional.of(mockUser));  // Optional 타입으로 감싼다.

        User user = userService.getUserByName("John");

        assertEquals("John", user.getName());
    }
}

이때 Mockito 가 반환하는것들은 아래와 같은 방법들이 있다.

	.thenReturn(mockUser);
    
    .thenThrow(new RuntimeException("Error occurred"));
    
    .thenReturn(mockUser.getName());
    
    // 메소드 체이닝 가능
    .thenReturn(mockUser)
    .thenReturn(mockUser.getPosts());


@ExtendWith

JUnit5 에서 테스트 클래스나 메소드가 실행될때 추가적인 로직을 확장할 수 있다.
다음과 같이 사용해서 ApplicationContext를 관리하거나 스프링빈을 테스트 클래스에 주입한다.

@ExtendWith(SpringExtension.class)
class MySpringTests {
    // ...
}

이때 ExtendWith는 단순한 확장일뿐 통합테스트에 필요한 빈들은 SpringBootTest어노테이션으로 등록해야 한다.

Mockito 초기화

아래와 같은 방법으로 Mockito 어노테이션이 붙은 필드를 초기화한다. 하지 않으면 null을 반환하게 된다.

    @BeforeEach
    void setUp() {    	
        MockitoAnnotations.initMocks(this);
    }

JUnit5의 더 간단한 방법으로는 아래의 방법이다.

@ExtendWith(MockitoExtension.class)
class MyTestClass {
    // ...
}


@MockBean

실제 스프링 컨텍스트에 존재하는 @Bean과 다르게 임의로 만든 Bean을 말한다.
@MockBean 어노테이션을 사용하면 기존에 등록된 Bean을 Mock으로 대체하거나, 컨텍스트에 존재하지 않는 Bean을 M ock으로 새롭게 등록할 수 있다.
@Bean은 주로 외부 라이브러리나 설정등을 스프링컨텍스트( IoC )에 등록할때 사용한다.

따라서 테스트시에는 @MockBean을 사용하여 임의의 상황( @Mock객체 )을 스프링 컨텍스트( IoC )에 주입하여 테스트할 수 있다.
IoC에 주입되었으므로 통합테스트에서도 사용된다.
@MockBean을 통해서 통합테스트를 빠르게 테스트하기 위해서도 사용된다.

@Mock

@Mock 어노테이션은 테스트 클래스 내부에서 Mock객체를 생성하고 관리하는데 사용된다.
단순히 Mock객체가 생성되었을뿐 스프링 컨텍스트와 별개로 존재한다.
외부 환경에 의존하지 않고 독립적으로 테스트를 수행해 목적에 따라 동작을 정의할 수 있고 메소드 호출에 대한 반환 값을 지정하거나 예외를 발생시킬 수 있다.

진짜 객체를 추상화된 가짜 객체로 만들어서 Mockito환경에 주입하는것을 말한다.
아래와 같은 코드로 테스트환경에서 인증이 된것처럼 상황을 만들 수가 있다.

    @Mock
    private AuthenticationManager authenticationManager;
    // 어떤 인자로든 호출만 되면 `any()` -> authentication 반환
    Mockito.when(authenticationManager.authenticate(any())).thenReturn(authentication);


@InjectMocks

@Mock으로 만든 Mock객체들을 실제로 IoC에 존재하는 객체에 @InjectMocks를 붙여서 주입한다.

이러한 주입의 장점은 의존성을 관리할 필요없이 자동으로 만들어지므로 편하게 사용할 수 있다.
또한 어떠한 의존성이 주입되었는지 한눈에 보이므로 가독성이 좋아진다.
아래에서 실제 메소드를 Mock객체로 호출했으므로 직접 구현할 필요가 없어진다.

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
    @Mock
    UserRepository userRepositoryMock;
	
    // 서비스가 레파지토리를 의존하므로 가짜를 주입한다.
    @InjectMocks
    UserService userService;

    @Test
    void testGetData() {
        User mockUser = new User();
        when(userRepositoryMock.fetchData()).thenReturn(mockUser);

        User user = userService.getUser();

        assertEquals(mockUser, user);
    }
}

위와 같은 방법으로 Mock객체를 주입해서 원하는 결과가 나오는지 테스트할 수 있다.

@Spy

실제 객체를 만들어 Mockito 환경에 주입하고, 해당 객체의 일부 메서드만 가짜 구현으로 대체하여 테스트한다.
어떤 인자로 몇번 호출되었는지등을 검증할때 사용한다.
단위테스트에서는 이런 어노테이션 사용으로 가능한 독립적으로 다른 컴포넌트를 의존하지 않는 테스트를 권장한다.

    @Spy
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    
    @Test
    public void bCrypt_test(){
        String pw = "1234";
        String enc = bCryptPasswordEncoder.encode(pw);
        System.out.println(enc);
    }

@SpyBean

스프링의 IoC 컨테이너에 Spy 객체를 주입한다.
일부메소드를 가로채어 원하는 동작( ex 가짜응답을 반환 )을 수행하도록 한다.


테스트 후 배포는 ?

Docker를 이용해서 우분투 환경을 만든다.
우분투 환경에서 테스트 후 배포한다.

개발할때 테스트가 완료되면 CI/CD 파이프라인을 통해서 자동배포 프로세스를 실행한다.
배포를 하려면 먼저 트리거를 만든다.
예를들어 main branch에 머지되면 -> Test 서버에 배포( 우분투 + 환경변수 세팅등..)

Test 서버안에서 모든 작업을 개발자가 직접 하는것은 귀찮으니 배포 전용 스크립트를 작성한다. -> deploy.yml
Docker를 이용해서 스크립트를 작성할 수 있다. -> docker-compose.yml

Test서버에 올렸을때 스크립트가 해야하는 일은 ?
1. 환경설치 ( OS + 소프트웨어 )
2. 환경변수 설정
3. Junit 테스트
4. jar 파일 빌드

이후 파일은 (jar/스크립트) s3(파일저장소)에 들어가게 된다. ( 엘라스틱빈스톡 생성시 s3가 만들어짐 )
s3에 들어간후 ec2로 푸쉬로직이 실행된다. ( AWS에 기능이 있음 - 코드디플로이 )

즉, 트리거에는 소스코드와 배포 스크립트가 존재하게 된다.
트리거의 방법에는 tredis / jenkins / githubAction(훅) 등이 있다.

profile
작은것부터

0개의 댓글