본문 바로가기
쿤즈 Dev/Spring Boot

[Spring Boot] Service 테스트 코드 작성하기 (feat. Kotlin)

by :)Koon 2022. 6. 7.

지난 포스팅에서는 Entity를 생성하고 Repository를 만들어서 테스트 코드를 작성해 보았습니다.

2022.06.04 - [쿤즈 Dev/Spring Boot] - [Spring Boot] Repository 테스트 코드 작성하기(feat. Kotlin)

 

[Spring Boot] Repository 테스트 코드 작성하기(feat. Kotlin)

스프링 부트 프로젝트를 진행하면서 이제는 선택이 아닌 필수가 되는 것 중 하나가 바로 테스트 코드의 작성입니다. 테스트 코드는 무작정 작성해도 되지만 잘 작성해 놓으면 여러 가지 오류에

koonsland.tistory.com

Repository를 만들 때에는 간단하게 JpaRepository를 상속받았고 API하나를 추가하여 테스트를 진행해 보았습니다.

이번 포스팅에서는 Service 코드를 작성하고 테스트 코드 작성을 진행해 보도록 하겠습니다.


Service 코드 작성하기

지난 포스팅에서 작성한 엔티티는 다음과 같습니다.

@Entity
class Member(
    var name: String,
    var age: Int,
) {
    @Id
    @GeneratedValue
    @Column(name = "member_id")
    val id: Long? = null
    
    
    //== 비지니스 로직==//
    fun updateMember(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}

그렇다면 우선 작성할 Service 로직의 API들을 인터페이스로 작성해 보도록 하겠습니다. 인터페이스로 작성하는 이유는 구현해야 할 API들을 정확하게 명시할 수 있기 때문입니다.

interface MemberService {
    // 회원 생성
    fun addMember(memberReq: MemberReqDto): Long
    // 회원 전체 조회
    fun findAllMember(): List<MemberRespDto>
    // 회원 Id 조회
    fun findMemberById(memberId: Long): MemberRespDto
    // 회원 이름 조회
    fun findMemberByName(memberAge: Int): MemberRespDto
    // 회원 수정
    fun updateMember(memberId: Long, memberReq: MemberReqDto): Long
    // 회원 삭제
    fun deleteMember(memberId: Long): Unit
}

각 함수들의 Parameter로 들어가는 Dto들은 추가적으로 생성했습니다.

data class MemberReqDto(
    val name: String,
    val age: Int
)

data class MemberRespDto(
    val name: String,
    val age: Int
)

회원 서비스 인터페이스에는 생성, 조회, 수정, 삭제인 CRUD 기능들의 API를 명시하였습니다. 완벽한 기능이 아닌 간단한 기능들을 각각 구현해 볼게요.

@Service
@Transactional(readOnly = true)
class MemberServiceImpl(
    val memberRepository: MemberRepository
): MemberService {

    @Transactional
    override fun addMember(memberReq: MemberReqDto): Long {
        val findMember = memberRepository.findByName(memberReq.name)
        if(findMember.isPresent)
            throw IllegalStateException("이미 존재하는 사용자입니다")

        val member = Member(memberReq.name, memberReq.age)

        memberRepository.save(member)

        return member.id!!
    }

    override fun findAllMember(): List<MemberRespDto> {
        val members = memberRepository.findAll()

        return members.map { MemberRespDto(it) }.toList()
    }

    override fun findMemberById(memberId: Long): MemberRespDto {
        val findMember = memberRepository.findById(memberId)
            .orElseThrow { IllegalStateException("회원을 찾을 수 없습니다") }

        return MemberRespDto(findMember)
    }

    override fun findMemberByName(memberName: String): MemberRespDto {
        val findMember = memberRepository.findByName(memberName)
            .orElseThrow { IllegalStateException("회원을 찾을 수 없습니다") }

        return MemberRespDto(findMember)
    }

    @Transactional
    override fun updateMember(memberId: Long, memberReq: MemberReqDto): Long {
        val findMember = memberRepository.findById(memberId)
            .orElseThrow { IllegalStateException("회원을 찾을 수 없습니다") }

        findMember.updateMember(memberReq.name, memberReq.age)

        return findMember.id!!
    }

    @Transactional
    override fun deleteMember(memberId: Long) {
        val findMember = memberRepository.findById(memberId)
            .orElseThrow { IllegalStateException("회원을 찾을 수 없습니다") }

        memberRepository.delete(findMember)
    }
}

구현체를 간단하게 만들어 보았습니다. 이렇게 만든 함수들을 테스트해보도록 하겠습니다.


Service 코드 테스트 작성

테스트코드를 만들기 위해서 먼저 테스트 쪽에 파일을 생성해야 합니다. 아래와 같이 테스트 프로그램을 작성해 보았습니다.

@SpringBootTest
@Transactional
internal class MemberServiceImplTest(
    @Autowired val memberService: MemberService,
    @Autowired val memberRepository: MemberRepository
) {

    @Test
    fun `save member`()
    {
        //given
        val newMember = MemberReqDto("test1", 30)

        //when
        val memberId = memberService.addMember(newMember)
        val findMember = memberRepository.findById(memberId).get()

        //then
        assertEquals(memberId, findMember.id)
    }
}

테스트를 하기 위해서 @SpringBootTest 애노테이션과 롤백은 위한 @Transactional 애노테이션을 붙여주었습니다. 그리고 우리가 만든 MemberService 클래스와 MemberRepository 클래스를 주입받도록 합니다. 테스트 코드에서는 주입을 위해서 @Autowired 애노테이션을 사용합니다.

 

이제 테스틑 할 준비가 되었기 때문에 테스트 코드 작성을 해봅니다. 테스트 코드는 Repository와 마찬가지로 given, when, then 순서로 작성합니다.

 

회원을 저장하는 코드는 MemberReqDto를 이용해서 회원정보를 받아서 저장합니다. 그리고 이 값이 실제로 저장되었는지 확인하기 위해서 MemberRepository를 이용해서 값을 가져와 assertEquals() 함수를 이용해서 비교합니다.

 

테스트가 정상적으로 되었다면 다른 테스트 코드도 실행해 봅니다. 이번에는 실패 코드를 실행해 보도록 하겠습니다.

@SpringBootTest
@Transactional
internal class MemberServiceImplTest(
    @Autowired val memberService: MemberService,
    @Autowired val memberRepository: MemberRepository
) {

    @Test
    fun `save member`()
    {
        //given
        val newMember = MemberReqDto("test1", 30)

        //when
        val memberId = memberService.addMember(newMember)
        val findMember = memberRepository.findById(memberId).get()

        //then
        assertEquals(memberId, findMember.id)
    }


    @Test
    fun `member save fail`()
    {
        //given
        val member = Member("koon", 30)
        memberRepository.save(member)
        val newMember = MemberReqDto("koon", 30)

        //when
        val assertThrows = assertThrows(IllegalStateException::class.java) {
            memberService.addMember(newMember)
        }

        //then
        assertEquals(assertThrows.message, "이미 존재하는 사용자입니다")
    }
}

MemberRepository를 이용해서 먼저 회원을 저장합니다. 그리고 동일한 회원이 저장될 때 에러를 발생하는 테스트를 진행해 보겠습니다. 저장한 이후에 동일한 값을 저장하면 MemberService에서는 IllegalStateException이 발생하도록 만들었습니다. 그래서 테스트 코드에서는 assertThrows() 함수를 이용해서 동일한 Exception이 발생하는지 테스트를 진행합니다.

 

Exception이 발생하면 이를 변수로 받아서 assertEquals() 함수를 이용해서 동일한 메세지인지 확인 테스트도 가능합니다.


이번 포스팅에서는 Service 로직에 대한 테스트 코드 작성을 하는 방법을 알아보았습니다. 사실 조금 더 세밀하고 정확하게 테스트를 진행해야 합니다. 테스트 코드가 잘 되면 그만큼 프로그램의 로직 정확도가 올라간다는 의미입니다.

 

실무에서 적용할때에 적용하는 부분도 있지만 공부할 때에는 열심히 적용하는 공부를 하는 것이 필요합니다. 도움이 되셨으면 합니다. 이상입니다.

댓글