본문 바로가기
  • 개발공부 및 일상적인 내용을 작성하는 블로그 입니다.
Spring basic

스프링 입문 : 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 스프링 통합 테스트

by 방구석 대학생 2021. 11. 10.

"인프런의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의를 듣고 작성한 글 입니다."

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8

 

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., 스프링 학습 첫 길잡이! 개발 공부의 길을 잃지 않도록 도와드립니다. 📣 확인해주세

www.inflearn.com

 

 

스프링 통합 테스트

이번엔 스프링과 DB 까지 모두 합쳐서 테스트하는 통합 테스트를 해보자.

이전에 해봤던 테스트들은 스프링과 전혀 관계없이 순수 자바 코드만을 가직 했던 테스트이다.

제대로 데이터베이스와 어플리케이션을 연동하고 나서 부터는 순수 자바 코드만으로 테스트를 해보는건 힘들다.(데이터베이스 커넥션 정보와 같은 것들도 스프링 부트가 들고 있는 상황이기 때문)

이제부터는 테스트를 스프링과 엮어서 해보자.

 

MemberServiceIntegrationTest 라는 파일을 만들고 일단 MemberServiceTest 파일의 내용을 복붙 해준 다음 @SpringBootTest, @Transactional 어노테이션을 달아준다.

여기서 @SpringBootTest 어노테이션은 스프링 컨테이너와 테스트를 함께 실행할 수 있게 해주는 어노테이션이다.

 

- MemberServiceIntegrationTest.java

@SpringBootTest
@Transactional
public class MemberServiceIntegrationTest {
    
    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach(){
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository); 
    }


    @AfterEach 
    public void afterEach(){
        memberRepository.clearStore();
    }

    @Test
    void 회원가입() {

        // given
        Member member = new Member();
        member.setName("spring");

        // when
        Long saveId = memberService.join(member);

        // then
        Member findMember = memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMember.getName()); 

    }

    @Test
    public void 중복_회원_예외(){
        // given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");
        // when
        memberService.join(member1);
        
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다."); 

        // then
    }
    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

 

여기서 우리는 의존성 주입을 스프링 컨테이너를 통해서 해줘야 하기 때문에 코드를 조금 변경해줘야 한다.

의존성 주입을 해줄때 테스트는 그냥 편하게 필드 주입 방식으로 자동 의존관계를 설정 해줘도 된다.

기본 코드는 생성자를 통한 의존성 주입을 해주는 게 좋은데 반해, 테스트는 다른데서 테스트 클래스를 가져다가 사용하고 그럴게 아니기 때문에 그냥 의존성 자체를 가져다 쓰기만 하면 끝이다.

그리고 MemoryMemberRepository 인스턴스를 쓰던것도 MemberRepository 로 바꿔준다.

(이렇게 해놔도 구현체는 SpringConfig 에서 정의해 놓은대로 JdbcMemberRepository 가 올라온다.) 

 

필드 주입을 통한 의존성 주입을 해주기 때문에 beforeEach() 메소드를 통해 의존성을 주입해주는 코드는 필요없으니 지운다.

데이터베이스에 직접 접근해서 데이터를 관리하며, @Transactional 어노테이션을 사용하고 있으므로 afterEach() 메소드 또한 제거한다.(@Transactional 어노테이션에 대해서는 조금 뒤에 설명한다.)

 

위와 같은 설명대로 코드를 수정하면 다음과 같다.

- MemberServiceIntegrationTest.java

@SpringBootTest
@Transactional
public class MemberServiceIntegrationTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    void 회원가입() {

        // given
        Member member = new Member();
        member.setName("spring");

        // when
        Long saveId = memberService.join(member);

        // then
        Member findMember = memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMember.getName());

    }

    @Test
    public void 중복_회원_예외(){
        // given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");
        // when
        memberService.join(member1);

        // then
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

    }
}

 

그럼 테스트를 해보기 전 기존에 강의를 들으며 데이터베이스에 저장했던 데이터들을 모두 지워주자.

delete from member;

 

* 실무에서는 테스트를 진행할 경우 실제 운영하는 데이터베이스 에서 수행하는 것이 아니라 따로 테스트 전용 데이터베이스를 두고 수행한다.(실제 운영하는 DB 에서 테스트 하면 진짜 큰일남)

 

이 상태로 회원가입 테스트만 따로 돌려보면 실제로 스프링이 띄워지면서 테스트가 잘 수행되는 것을 확인 할 수 있다.

- 테스트 결과 띄워진 로그

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.5)

2021-11-10 21:44:37.226  INFO 27128 --- [           main] h.h.s.MemberServiceIntegrationTest       : Starting MemberServiceIntegrationTest using Java 15.0.2 on DESKTOP-5OLJ02R with PID 27128 (started by tnrdu in C:\Users\tnrdu\OneDrive\바탕 화면\각종 폴더\학습용 폴더\인프런-스프링 입문(코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기슬)\hello-spring)
2021-11-10 21:44:37.228  INFO 27128 --- [           main] h.h.s.MemberServiceIntegrationTest       : No active profile set, falling back to default profiles: default
2021-11-10 21:44:38.916  INFO 27128 --- [           main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page: class path resource [static/index.html]
2021-11-10 21:44:39.287  INFO 27128 --- [           main] h.h.s.MemberServiceIntegrationTest       : Started MemberServiceIntegrationTest in 2.377 seconds (JVM running for 3.518)
2021-11-10 21:44:39.322  INFO 27128 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-11-10 21:44:39.416  INFO 27128 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.

 

그런데 데이터베이스를 확인해보면 회원가입 테스트를 통해 데이터베이스 안에 삽입시키는 spring 데이터가 삽입되어 있지 않은 것을 확인할 수 있을 것이다.

그럼 여기서 @Transactional 어노테이션을 주석 처리하고 회원가입 테스트를 다시 수행해보자.

테스트가 정상적으로 잘 수행되고 나면 이번엔 아까와 달리 데이터베이스에 spring 데이터가 정상적으로 삽입되어 있는 것을 볼 수 있다.

 

그런데 앞전에 한 번 테스트 코드를 다뤄볼 때 정리했듯 모든 테스트는 독립적으로 수행되어야 하며, 같은 테스트를 여러번 반복할 수 있어야 한다고 했다.

여기서 @Transactional 어노테이션이 계속 주석 처리된 상태로 다시 한번 회원가입 테스트를 수행하면 무슨 일이 벌어질까?

정답은 '테스트가 정상적으로 통과되지 못하고 에러가 발생한다.' 이다.

왜냐하면 테스트에서 spring 문자열을 회원 데이터로 member 테이블에 삽입하고 있는데 테이블에는 똑같은 이름의 데이터가 존재하기 때문에, 테이블 내에 동일한 이름이 존재할 시 발생하게끔 만들어 두었던 IllegalStateException 이 발생하게 되는 것이다.

 

그렇다면 어떻게 해야할까?

저장공간을 메모리에 했을 때 처럼 afterEach() 메소드 같은 걸 만들어야 할까?

그럴 필요가 없다.(여기서 스프링이 기가막힌걸 제공한다.)

데이터베이스는 기본적으로 트랜잭션이 이라는 개념이 있다.

DB 에 데이터를 삽입한 다음에 사실 커밋 까지 해줘야 DB 에 해당 변경사항이 반영된다.

그게 아니면 오토 커밋 모드라고 해서 자동으로 데이터베이스를 커밋해주는 것도 있다.

 

그런데 여기서 만약 테스트를 수행한 다음 데이터베이스 자체를 롤백 해줘버리면 어떨까?

데이터베이스를 롤백 해버리면 DB 에 반영되었던 변경내역이 모두 사라지게 된다.

그런 방식으로 테스트를 진행할 수 있게끔 해주는 것이 바로 @Transactional 어노테이션 이다.

이 어노테이션을 테스트 케이스에 붙여주면 테스트가 수행되는 동안 어떤 형태로든 데이터베이스에 변경 내역이 만들어 졌다고 해도 테스트가 끝나는 시점에 데이터베이스가 완전히 롤백 되버리면서 적용되었던 변경 내역이 모두 사라진다.

 

@Transactional 어노테이션에 대한 요약

이 어노테이션에 대해 요약해서 설명하자면

'테스트 케이스에 이 어노테이션이 있으면 테스트 시작 전에 트랜잭션(데이터베이스 통신 단위) 을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.' 가 될 수 있겠다.

 

이제 전체 테스트를 돌려보면 정상적으로 모든 테스트 들이 잘 통과되는 것을 확인할 수 있다.

 

단위 테스트와 통합 테스트

기존에 순수한 자바 코드만으로 테스트를 진행했던 MemberServiceTest 와 이번에 작성한 MemberServiceIntegrationTest 를 전체적으로 수행시켜 보면

MemberServiceTest 는 순식간에 테스트가 끝나고 MemberServiceIntegrationTest 는 스프링을 동작 시키고 데이터베이스와 연동하는 작업을 수행하느라 상대적으로 테스트 시간이 길게 걸리는 것을 알 수 있다.

 

여기서 MemberServiceTest 처럼 순수하게 자바 코드로만 수행하는 것을 단위 테스트라고 하고 MemberServiceIntegrationTest 처럼 스프링 컨테이너 까지 연동한 테스트를 통합 테스트 라고 한다.

그런데 보통 단위 테스트가 통합 테스트 보다 훨씬 좋은 테스트일 확률이 높다.(반드시 좋은 테스트라고 하기엔 애매하다, 그렇기에 확률이 높다고 하는것 - 만약 스프링 컨테이너를 반드시 올려서 테스트를 진행해야 하는 상황이면 테스트 설계가 잘못 되었을 확률이 높다.)

그렇기 때문에 단위 별로 쪼개서 잘 테스트를 수행할 수 있도록 하는 것과 스프링 컨테이너 없이 테스트를 해볼 수 있는 훈련을 해보는 것이 좋다.

 

다음 시간부터는 순수 JDBC 가 아닌 스프링 JdbcTemplate 으로 바꿔서 진행해보자.