service , controller 등에서는 mock 을 이용해 로직이 잘 동작하는지를 주목적으로 테스트 코드를 작성 하지만, Repository Test는 실제로 데이터가 DB에 잘 이동하는지 확인하기 위해서 하는 목적이 큰 만큼 mock을 사용하는 것이 아닌 실제 DB를 이용하는 것을 추천한다.
실제 DB는 사용 중인 개발/운영 서버의 DB에 테스트 후에 Transaction을 하는 방법도 있겠지만, 데이터가 많거나 실제 운영 중인 서버라면 속도 지연의 원인이 될 수 있기에 테스트 DB는 따로 설정하는 것이 좋다.
이를 위해 휘발성 DB인 H2를 이용하도록 하겠다.
testRuntimeOnly 'com.h2database:h2'
테스트를 진행할 테이블은 Members 테이블이며, 아래와 같은 구조를 가지고 있다.
@Alias("member")
@Getter
@NoArgsConstructor
public class Member {
private Long id;
private SnsType snsType;
private String snsId;
private String email;
private String profile;
private String nickname;
private String address;
private String gender;
private Integer age;
private String phone;
private Role role;
@Builder
public Member(
SnsType snsType,
String snsId,
String email,
String profile,
String nickname,
String address,
String gender,
Integer age,
String phone
) {
this.snsType = snsType;
this.snsId = snsId;
this.email = email;
this.profile = profile;
this.nickname = nickname;
this.address = address;
this.phone = phone;
this.gender = gender;
this.age = age;
this.role = Role.MEMBER;
}
Repository는 아래와 같다. ( mybatis )
@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final SqlSession mysql;
@Transactional
public void save(Member member) {
mysql.insert("MemberMapper.save", member);
}
public Optional<Member> findById(Long id) {
return Optional.ofNullable(mysql.selectOne("MemberMapper.findById", id));
}
}
아래는 전체 코드이다.
테스트 코드는 어떤 환경, 조건에서든 동일하게 100% 통과할 수 있는 코드를 작성하는 만큼 되도록 고정된 값보다는 랜덤한 값을 사용하는 것이 좋다.
아래의 코드는 편의를 위해 전화번호는 전화 번호 형식으로, email은 email 형식으로 값을 생성하여 반환하고 있지만 실제로는 항상 이러한 고정된 형식으로 값을 전달 받는 것이 아니기에 그에 맞는 값을 생성해야할 필요가 있다.
또한 테스트 코드는 누가 , 어떤 것을 목적으로 해당 코드를 작성했는지 한 눈에 파악할 수 있도록 하는 것이 유리하기에, given when then을 주석으로 많이 활용하는 편이다.
++ Mybatis를 사용할 때 Repository에 @mapper을 이용해 자동으로 매핑 되는 경우는 상관없지만, 위의 코드와 같이 다른 방식으로 작성 후 MybatisConfig와 같이 직접 명시해준 경우 아래 코드에서 @Import를 통해 해당 파일을 직접 명시해주어야한다.
@Import(MyBatisConfig.class)
@Import(MemberRepository.class)
전체 코드
//실제 DB에 값이 저장이 되고, 읽을 수 있는지를 확인해야 하기에 @MyBatisTest 를 사용하여야 한다.
@MyBatisTest()
//application-test.yml을 따로 지정하여 사용할 경우 추가해야함
@ActiveProfiles("test")
class MemberRepositoryTest1{
// 직접 repository에 접근할 것이기에 mock 사용하지 않음
@Autowired
private MemberRepository memberRepository;
// 추후 snsType 중 랜덤한 값을 가져오기 위해 작성
private final SnsType[] snsTypes = SnsType.values();
// 랜덤 값 생성을 위해 작성
private final Random random = new Random();
// 랜덤한 값을 가진 Member를 중복된 코드를 최소화하여 생성하기 위해 작성
public Member createRandomMember() {
return Member.builder()
.snsType(randomSnsType())
.snsId(randomSnsId())
.email(randomEmail())
.address(randomString())
.profile(randomProfile())
.age(randomAge())
.gender(randomGender())
.nickname(randomString())
.phone(randomPhone())
.build();
}
// SnsType.class 내부의 값 중 랜덤한 값을 반환
private SnsType randomSnsType() {
return snsTypes[random.nextInt(snsTypes.length)];
}
// 랜덤한 UUID 값을 반환
private String randomSnsId() {
return UUID.randomUUID().toString();
}
// 랜덤 길이의 이메일을 포함한 이메일 반환
private String randomEmail() {
SnsType SnsType = snsTypes[random.nextInt(snsTypes.length)];
UUID uuid = UUID.randomUUID();
String uuidString = uuid.toString();
int nicknameLength = random.nextInt(1,20);
return uuidString.substring
(random.nextInt(nicknameLength))
+ "@"
+ SnsType
+ ".com";
}
// url 형식의 랜덤한 값을 반환
private String randomProfile() {
return "<https://www.test.com/profile/>" + UUID.randomUUID();
}
// 1, 100까지 중 랜덤한 값을 반환
private Integer randomAge() {
return random.nextInt(1,100);
}
// 남 or 여 중 하나의 값을 반환
private String randomGender() {
return random.nextBoolean() ? "남" : "여";
}
private String randomPhone() {
// 전화번호 형식의 랜덤한 값을 반환
return String.format("010-%04d-%04d", random.nextInt(10000), random.nextInt(10000));
}
// 랜덤한 String을 생성하고,생성된 String의 랜덤한 길이 만큼 반환
private String randomString(){
String address = new BigInteger(32, random).toString(32);
int addressLength = address.length();
return address.substring(0, random.nextInt(1,addressLength));
}
@Test()
@DisplayName("save_올바른 값")
void save() {
// Given
Member member = createRandomMember();
memberRepository.save(member);
// When
Member testMember = memberRepository.findById(member.getId()).orElseThrow(AssertionError::new);
// Then
assertEquals(member.getSnsType(), testMember.getSnsType());
assertEquals(member.getEmail() , testMember.getEmail());
assertEquals(member.getAddress() , testMember.getAddress());
assertEquals(member.getProfile() , testMember.getProfile());
assertEquals(member.getAge() , testMember.getAge());
assertEquals(member.getGender() , testMember.getGender());
assertEquals(member.getNickname() , testMember.getNickname());
assertEquals(member.getPhone() , testMember.getPhone());
}
@Test
@DisplayName("findById_올바른 값")
void findById_success() {
// Given
Member member = createRandomMember();
memberRepository.save(member);
// When
Member testMember = memberRepository.findById(member.getId()).orElseThrow(AssertionError::new);
// Then
assertEquals(member.getId(), testMember.getId());
}
@Test
@DisplayName("findById_올바르지 않은 값")
void findById_fail() {
// Given
Member member = createRandomMember();
memberRepository.save(member);
// When
Long testMemberId = member.getId();
Long id = testMemberId + random.nextInt(1, 9999);
Optional testMember = memberRepository.findById(id);
// Then
assertTrue(testMember.isEmpty());
}
}
'Springboot' 카테고리의 다른 글
fixtureMonkey, JakartaValidationPlugin 제약 조건 설정 (0) | 2024.10.06 |
---|---|
fixtureMonkey 객체 값 자동 생성 / springboot 테스트 코드 (0) | 2024.10.04 |
스프링부트 Repository 커스텀 (분리) (0) | 2024.09.23 |
Springboot 실행과 동시에 종료 오류 해결 (0) | 2024.09.04 |
API 응답 공통 포맷 ( Spring / Java ) (1) | 2024.09.02 |