[Spring Boot] 테스트 코드 : 시작하기 (JUnit)
테스트 코드의 필요성 및 실천 방법론
테스트 코드의 필요성과 "좋은 테스트 코드"를 작성하기 위한 방법론에 대해서 작성한 내용이다. 테스트 코드를 "왜" 작성하고 "어떠한 마음가짐"으로 임할 지에 대해서 서술해두었으니 읽어보면 좋을 것 같다.
https://sjh9708.tistory.com/238
JUnit이란?
자바를 위한 단위 테스트 프레임워크이다. 메소드, 클래스 및 패키지에 대한 단위 테스트를 작성하는 데 자주 사용된다.
- 간단하고 사용하기 쉬운 API를 제공하며, 테스트 케이스를 작성하고 실행하는 데 필요한 다양한 어노테이션 및 메서드를 제공한다.
- 테스트의 성공, 실패 또는 예외 처리와 같은 결과를 자동으로 보고한다.
- 개발자는 코드를 작성하기 전에 해당 코드의 동작을 정의하는 테스트 케이스를 작성하고, 이 테스트 케이스를 사용하여 코드를 구현하고 테스트한다. 이를 통해 코드의 품질을 향상시키고 버그를 빠르게 발견할 수 있다.
테스트 코드 기본
Spring Boot에서 테스트 코드는 주로 프레임워크에서 test 코드를 작성하는 디렉터리로 src/test/java에 작성한다.
여기에 테스트 클래스를 하나 생성해주자.
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@Transactional // 각각의 테스트 메서드에 대해 트랜잭션을 시작하고, 테스트가 종료되면 롤백
class TagServiceTest {
}
기본적인 테스트 코드의 작성 틀이다.
@SpringBootTest 어노테이션은 Spring Boot 애플리케이션을 테스트하기 위한 환경을 설정한다. 이는 스프링의 ApplicationContext를 로드해주어, 테스트 환경에서도 스프링 환경과 Bean들을 주입받아서 사용할 수 있게 된다.
@Transactional 어노테이션은 각각의 테스트 메서드에 대해 트랜잭션을 시작하고, 테스트가 종료되면 롤백한다. 이렇게 하면 데이터베이스 등에 테스트 코드의 내용이 반영되지 않으며 각각의 테스트가 독립적으로 실행되고 서로의 상태에 영향을 주지 않게 된다.
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@Transactional // 각각의 테스트 메서드에 대해 트랜잭션을 시작하고, 테스트가 종료되면 롤백
class TagServiceTest {
@BeforeEach
public void before(){
System.out.println("Test Before");
}
@AfterEach
public void after(){
System.out.println("Test After");
}
}
@BeforeEach와 @AfterEach 어노테이션을 사용하여 테스트 전후 처리를 할 수 있다.
각각의 테스트 메서드마다 적용되어, 실행되기 전과 후에 실행될 코드를 정의하여 필요한 초기화 작업이나 마무리 작업을 작성할 수 있다.
import org.junit.jupiter.api.AfterEach;
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 org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@Transactional // 각각의 테스트 메서드에 대해 트랜잭션을 시작하고, 테스트가 종료되면 롤백
class TagServiceTest {
@Autowired
private TestService testService;
@BeforeEach
public void before(){
System.out.println("Test Before");
}
@AfterEach
public void after(){
System.out.println("Test After");
}
@Test
@DisplayName("두 수가 일치해야 성공")
public void test() throws Exception {
// Given
int number = 0;
System.out.println("Given");
// When
int generateNumber = testService.generate();
System.out.println("When");
// Then
assertThat(generateNumber).isEqualTo(number);
System.out.println("Then");
}
@Test
@DisplayName("두 수가 일치하지 않아야 성공")
public void test2() throws Exception {
// Given
int number = 0;
System.out.println("Given");
// When
int generateNumber = testService.generate();
System.out.println("When");
// Then
assertThat(generateNumber).isNotEqualTo(number);
System.out.println("Then");
}
}
@Service
public class TestService {
public int generate(){
return 0;
}
}
@Test 어노테이션을 사용하여 테스트 메서드를 정의한다. 각 테스트 메서드는 테스트 케이스를 나타내며, 테스트할 기능을 작성한다.
@DisplayName 어노테이션은 테스트 메서드의 이름을 지정할 수 있다.
BDD 스타일에 의한 테스트 메서드의 구성은 Given, When, Then으로 구성된다.
Given: 주어진 상황을 설정하는 부분.
When: 테스트할 기능을 호출하고 실행하는 부분.
Then: 기대한 결과를 검증하는 부분. 특정한 결과나 동작이 예상대로 수행되었는지 확인한다.
위의 테스트는 Given으로 주어진 0과, generate() 메서드로 생성된 정수가 일치하는 지의 여부를 확인하는 코드이다.
아래의 테스트는 반대로 일치하지 않는지의 여부를 확인하는 코드이다.
따라서 위의 테스트는 성공할 것이고, 아래의 테스트는 실패하여 통과되지 못할 것이다.
테스트 코드 실행
왼쪽 사진 : 테스트 클래스의 @Test를 전부 실행시키려면 클래스 단위로 Run 시키면 된다.
오른쪽 사진 : 테스트 클래스의 @Test를 하나만 실행시키려면 메서드를 우클릭한 후 Run 시키면 된다.
InteliJ에서 테스트 결과를 확인할 수 있다. 다음과 같이 테스트가 통과한 것과, 통과하지 못한 것들을 확인할 수 있다.
테스트 코드 작성 프로세스 살펴보기
이제 실제 요구사항에 따른 단위 테스트 코드를 작성해보자. 지금부터 작성되는 테스트 코드는 가장 간단한 형태로 테스트 코드를 작성하는 방법을 알아보기 위해서 작성된 내용이며, 뒤에서 한계점 및 극복할 점을 언급하겠다.
테스트 케이스 작성
태그에 관련된 요구사항에 대해서 테스트 코드를 작성할 것이다. 요구사항은 다음과 같다.
1. 회원은 자기 소유의 태그 리스트를 가진다.
2. 회원은 태그 이름과 태그 내용을 지정하여 추가할 수 있다.
3. 다른 회원간은 상관 없지만, 같은 회원 소유의 동일한 이름의 태그를 추가할 수 없다.
4. 회원이 자신이 소유한 태그 리스트를 조회 가능하다.
@SpringBootTest
@Transactional
class TagServiceTest {
@BeforeEach
public void before(){
}
@Test
@DisplayName("태그 추가 성공")
public void successTagInsert() throws Exception {
}
@Test
@DisplayName("태그 추가 실패 : 존재하지 않는 회원")
public void failTagInsertCaseNotExistMember() throws Exception {
}
@Test
@DisplayName("태그 추가 실패 : 해당 회원에 중복된 태그이름이 존재")
public void failTagInsertCaseDuplicateName() throws Exception {
}
@Test
@DisplayName("태그 조회 : 해당 회원 소유의 태그가 1개일 때 조회 성공")
public void successTagSelectByMemberV1() throws Exception {
}
@Test
@DisplayName("태그들 조회 : 해당 회원 소유의 태그가 여러개일 경우 태그 리스트 조회 성공")
public void successTagSelectByMemberV2() throws Exception {
}
}
TDD 개발 방법론에 따라서, 실제 코드를 작성하기 전에 테스트 케이스를 먼저 작성하였다. 요구사항에 맞추어 테스트 메서드를 작성해주자.
실제 개발 수행
@Service
@AllArgsConstructor
@Transactional(readOnly = true)
public class TagServiceImpl implements TagService{
private final TagRepository tagRepository;
private final MemberRepository memberRepository;
private final ModelMapper modelMapper;
private final PasswordEncoder encoder;
@Override
@Transactional
public Long addTag(TagRequestDto inputTag, Long memberId) {
Member member = memberRepository.findMemberByMemberId(memberId);
if(member == null){
throw new NoSuchElementFoundException("존재하지 않는 회원입니다");
}
if(tagRepository.isExistByMemberOwnTagName(memberId, inputTag.getName())){
throw new AlreadyExistElementException("이미 회원에게 존재하는 태그 이름입니다.");
}
Tag tag = modelMapper.map(inputTag, Tag.class);
tag.setMember(member);
Tag result = tagRepository.save(tag);
return result.getTagId();
}
@Override
public MemberWithTagDto getTagInfoByMember(Long memberId) {
Member member = memberRepository.findAllLeftFetchJoin(memberId);
MemberWithTagDto dto = modelMapper.map(member, MemberWithTagDto.class);
return dto;
}
}
테스트 케이스와 비즈니스 로직에 맞는 실제 개발을 수행하였다. 서비스에 태그를 Save하는 기능과, 멤버 ID로 소유한 태그들을 모두 Select하는 코드를 작성해주었다.
테스트 코드 작성
@SpringBootTest
@Transactional // 각각의 테스트 메서드에 대해 트랜잭션을 시작하고, 테스트가 종료되면 롤백
class TagServiceTest {
@Autowired
private TagService tagService;
@Autowired
private MemberRepository memberRepository;
@Autowired
private TagRepository tagRepository;
@Autowired
private EntityManager em;
@Test
@DisplayName("태그 추가 성공")
public void successTagInsert() throws Exception {
}
@Test
@DisplayName("태그 추가 실패 : 존재하지 않는 회원")
public void failTagInsertCaseNotExistMember() throws Exception {
}
@Test
@DisplayName("태그 추가 실패 : 해당 회원에 중복된 태그이름이 존재")
public void failTagInsertCaseDuplicateName() throws Exception {
}
@Test
@DisplayName("태그 조회 : 해당 회원 소유의 태그가 1개일 때 조회 성공")
public void successTagSelectByMemberV1() throws Exception {
}
@Test
@DisplayName("태그들 조회 : 해당 회원 소유의 태그가 여러개일 경우 태그 리스트 조회 성공")
public void successTagSelectByMemberV2() throws Exception {
}
}
테스트를 위해서 사전에 필요한 작업들을 수행해주자.
1. TagService 등의 필요한 모듈들을 @Autowired를 통해서 주입시켜 주었다.
테스트 코드 : 태그 추가 성공 여부 확인
@Test
@DisplayName("태그 추가 성공")
public void successTagInsert() throws Exception {
// Given
Member member = createMember("hong@email", "홍길동");
memberRepository.save(member);
TagRequestDto request = new TagRequestDto("태그 이름", "태그 컨텐츠");
// When
Long insertTagId = tagService.addTag(request, member.getMemberId());
// Then
assertThat(insertTagId).isNotNull();
}
Service의 태그 추가 기능을 테스트하는 코드를 작성하였다.
assertThat(actual).isNull(expect) : 기대값(actual)이 Null이 아닌 경우 테스트를 통과시키는 기능이다.
따라서 태그가 정상적으로 추가되었다면, Null이 아닐 것이므로 해당 테스트를 통과할 것이다.
테스트 코드 : 태그 추가 실패 : 존재하지 않는 회원 케이스
@Test
@DisplayName("태그 추가 실패 : 존재하지 않는 회원")
public void failTagInsertCaseNotExistMember() throws Exception {
// Given
TagRequestDto request = new TagRequestDto("태그 이름", "태그 컨텐츠");
// When & Then
assertThrows(NoSuchElementFoundException.class, () -> {
Long insertTagId = tagService.addTag(request, Long.MAX_VALUE);
});
}
이번에는 Service의 태그 추가 기능 시, 태그를 소유한 회원 ID에 대한 파라미터가 잘못 들어왔을 경우를 테스트해본다.
assertThrows(SomeException.class, () -> code) : 해당 코드에서 임의의 SomeException이 발생 시 테스트를 통과하게 된다.
즉 유저가 존재하지 않는다면 Service는 NoSuchElementFoundException을 throw하므로, 테스트가 통과할 것이다.
테스트 코드 : 태그 추가 실패 : 중복된 이름의 태그 존재
@Test
@DisplayName("태그 추가 실패 : 해당 회원에 중복된 태그이름이 존재")
public void failTagInsertCaseDuplicateName() throws Exception {
// Given
Member member = createMember("hong@email", "홍길동");
memberRepository.save(member);
Tag exist = createTag("중복 태그", "컨텐츠1");
exist.setMember(member);
tagRepository.save(exist);
TagRequestDto request = new TagRequestDto("중복 태그", "태그 컨텐츠");
// When & Then
assertThrows(AlreadyExistElementException.class, () -> {
Long insertTagId = tagService.addTag(request, member.getMemberId());
});
}
마찬가지로 중복된 태그이름에 대해서도 예외가 발생하는 경우에 대한 케이스를 작성해두고, 예외가 발생 시 통과시키는 단위 테스트 코드를 작성시켜주었다.
테스트 코드 : 태그 조회 : 해당 회원 소유의 태그가 단일일 경우 조회 성공
@Test
@DisplayName("태그 조회 : 해당 회원에 해당하는 태그 조회 성공")
public void successTagSelectByMemberV1() throws Exception {
// Given
Member member = createMember("hong@email", "홍길동");
Member member2 = createMember("kim@email", "김길동");
member = memberRepository.save(member);
member2 = memberRepository.save(member2);
Tag tag1 = createTag("태그1", "컨텐츠1");
Tag tag2 = createTag("태그2", "컨텐츠2");
tag1.setMember(member);
tag2.setMember(member2);
member.setTags(List.of(tag1));
member2.setTags(List.of(tag2));
tag1 = tagRepository.save(tag1);
tag2 = tagRepository.save(tag2);
// When
MemberWithTagDto result = tagService.getTagInfoByMember(member.getMemberId());
// Then
assertThat(result.getTags().size()).isEqualTo(1);
assertThat(result.getTags().get(0).getTagId()).isEqualTo(tag1.getTagId());
assertThat(result.getTags().get(0).getTagId()).isNotEqualTo(tag2.getTagId());
}
회원 ID로 서비스를 호출 했을 때 회원 소유의 태그를 성공적으로 Read하는지를 테스트하는 코드이다.
해당 코드는 주어진 상황(Given)은, Tag1은 Member1의 소유이고, Tag2는 Member2의 소유이다.
이 때(When) Member1이 소유한 태그를 조회 시 아래의 내용들을 검증한다.
1. Member1가 소유한 태그의 개수가 1개라면 통과
2. Member1의 소유한 1개의 태그가 Tag1이면 통과
3. Member1이 소유한 1개의 태그가 Tag2가 아니면 통과
테스트 코드 : 태그 조회 : 해당 회원 소유의 태그가 여러개일 경우 리스트 조회 성공
@Test
@DisplayName("태그들 조회 : 해당 회원에 해당하는 태그들 조회 성공")
public void successTagSelectByMemberV2() throws Exception {
// Given
Member member = createMember("hong@email", "홍길동");
member = memberRepository.save(member);
Tag tag1 = createTag("태그1", "컨텐츠1");
Tag tag2 = createTag("태그2", "컨텐츠2");
tag1.setMember(member);
tag2.setMember(member);
member.setTags(List.of(tag1, tag2));
tag1 = tagRepository.save(tag1);
tag2 = tagRepository.save(tag2);
// When
// Member m = memberRepository.findMemberByMemberId(member.getMemberId());
MemberWithTagDto result = tagService.getTagInfoByMember(member.getMemberId());
// Then
assertThat(result.getTags().get(0).getTagId()).isEqualTo(tag1.getTagId());
assertThat(result.getTags().get(1).getTagId()).isEqualTo(tag2.getTagId());
}
마찬가지로 회원 ID로 서비스를 호출 했을 때 회원 소유의 복수의 태그들을 성공적으로 Read하는지를 테스트하는 코드이다.
해당 코드는 주어진 상황(Given)은, Tag1, Tag2가 Member1의 소유이다.
이 때(When) Member1이 소유한 태그를 조회 시 (Then) 두 개의 태그가 각각 Tag1, Tag2와 일치하는 지 검증한다.
// Then
// assertThat(result.getTags().get(0).getTagId()).isEqualTo(tag1Id);
// assertThat(result.getTags().get(1).getTagId()).isEqualTo(tag2Id);
assertThat(result.getTags())
.extracting(e -> e.getTagId())
.contains(tag1Id, tag2Id);
위의 assert 코드를 Equals가 아닌 포함 여부를 검증하는 코드로 다음과 같이 작성할 수도 있다.
다음 코드는 getTags()로 불러 온 Tag List를 extracting() 메서드를 통해서 특정 필드를 추출한 후, 해당 필드의 리스트에 비교 대상들이 포함되었는지를 contains()를 통해 검증한다.
// Then
// assertThat(result.getTags().get(0).getTagId()).isEqualTo(tag1Id);
// assertThat(result.getTags().get(1).getTagId()).isEqualTo(tag2Id);
// assertThat(result.getTags())
// .extracting(e -> e.getTagId())
// .contains(tag1Id, tag2Id);
assertThat(result.getTags())
.extracting(TagDto::getTagId)
.contains(tag1Id, tag2Id);
위의 코드를 메서드 참조 연산 문법을 사용해서 다르게 표현할 수도 있다. 이는 람다식을 간결하게 표현하기 위한 방법 중 하나이다.
TagDto :: getTagId는 TagDto 클래스의 getTagID() 메서드를 Element에 대해서 수행하는 연산이다.
Test Double : 단위 테스트에서의 모듈 의존성 격리
SpringBootTest는 통합 테스트의 목적으로 주로 사용되며 단위 테스트의 목적으로 사용할 때 극복해야 할 면들이 있다.
@SpringBootTest의 장점
1. 실제 애플리케이션과 유사한 환경에서 테스트를 수행하여 실제 동작을 더 정확하게 확인할 수 있다.
2. 실제 빈을 사용하므로 통합 테스트를 수행하기에 적절하다.
@SpringBootTest의 단점
1. 실행 속도가 느리다 -> 애플리케이션 컨텍스트 전체를 로드하고 Bean을 초기화해야 하기 때문이다.
2. 외부 의존성에 의해 테스트가 명확하지 않다. -> 문제를 분리하고 진단하기 어렵다.
위의 코드와 같이 작성된 코드는 통합 테스트의 목적으로는 적절하지만 단위 테스트 작성을 목표로 하였다면 역할이 너무 넓다. "작은 코드 단위를 독립적으로 검증" 해야 하는데 테스트의 범위가 너무 크고 포괄적이다.
SpringBootTest를 사용하여 테스트의 속도가 느리다.
TagService라는 모듈 하나를 테스트하려고 할 때 MemberService, Repository 등에 의존하게 되어 온전한 하나의 모듈의 테스트만 집중하여 수행했다고 볼 수 없다.
이를 다시 말하면 TagServiceTest의 테스트 실패 시, 책임의 소재가 불분명하다는 것이다. 오로지 TestService만을 검증해야 하는데 MemberService를 비롯한 의존하는 다른 모듈들에서의 버그를 살펴보아야 하는 현상이 발생한다.
해당 면모들을 보완하기 위해서 Test Double이라는 개념이 존재한다.
테스트 더블(Test Double)은 소프트웨어 테스트에서 실제 객체를 대신하여 사용되는 객체를 말한다. 이 객체는 테스트 중에 실제 객체의 행동을 대체하여, 테스트하고자 하는 대상 코드(Unit)를 독립적으로 검증할 수 있도록 도와준다.
Java에서는에서 JUnit을 확장시킨 테스트 라이브러리인 Mockito를 자주 사용한다. 자세한 내용들은 아래 포스팅에서 SpringBootTest와 Mockito를 비교한 내용을 살펴보도록 하자.
<함께 보기> Mock을 사용한 테스트 의존성 격리
https://sjh9708.tistory.com/219
함께 보기 : 계층(Controller, Repository, Service)별 테스트 코드 작성 전략
https://sjh9708.tistory.com/240
자주 사용되는 Assert 메서드
Assert는 JUnit에서 테스트 코드에서 특정 조건이 참인지를 검증하는 데 사용되는 메서드들의 모음이다. 그렇지만 주로 뒤에서 소개할 AssertJ 라이브러리에 밀려 잘 사용되지 않는 편이다.
값 비교 : assertEquals
int expected = 5;
int actual = someMethodThatReturnsFive();
assertEquals(expected, actual);
예상 값(expected)와 실제 값(actual)이 같으면 통과
객체 비교 : assertSame
String expected = "hello";
String actual = "hello";
assertSame(expected, actual);
예상 값(expected)와 실제 값(actual)이 동일한 객체를 참조하면 통과
조건 검증 : assertTrue
boolean condition = someCondition();
assertTrue(condition);
주어진 조건이 참이면 통과
Null 검증 : assertNotNull
Object obj = someMethodThatReturnsNonNullObject();
assertNotNull(obj);
인스턴스가 Null이 아니면 통과
예외 검증 : assertThrows
assertThrows(ArithmeticException.class, () -> divide(5, 0));
내부의 익명 함수(람다식)에서 해당 Excpetion이 발생하면 통과
AssertJ 라이브러리
assertThat 메서드는 AssertJ와 같은 라이브러리에서 제공된다.
다양한 Matcher들과 함께 사용된다. Matcher들은 예상되는 조건을 지정하여 실제 값과 비교하고 검증하는 데 사용하며 좀 더 풍부하고 가독성이 높은 검증 메서드를 작성할 수 있다.
다양한 AssertJ 메서드
값 비교
assertThat(actual).isEqualTo(expected);
isEqualTo() : Actual과 Expeted의 값이 동일한 지 비교
수치 대소 비교
assertThat(actual).isGreaterThan(5); // 5보다 큰지 여부
assertThat(actual).isBetween(1, 10); // 1과 10 사이에 있는지 여부
문자열 검증
String actualString = "Hello World";
assertThat(actualString).startsWith("Hello"); // 접두사 검증
assertThat(actualString).endsWith("World"); // 접미사 검증
assertThat(actualString).contains("Wo"); // 부분 문자열 포함 여부 검증
Null 검증
assertThat(actual).isNull(); // null 여부 검증
assertThat(actual).isNotNull(); // null이 아님을 검증
특정 조건을 만족하는 지 검증
assertThat(actual).matches(element -> element > 10);
matches() : 조건에 맞는 지를 검증한다.
컬렉션에서 조건에 맞는 Element 여부 검증
assertThat(actualList).anyMatch(element -> element > 10);
anyMatch() : 컬렉션에서 조건에 맞는 요소가 하나 이상 존재하는지 여부를 검증
컬렉션에서 포함 여부 검증
String[] actualList = {"재휘", "철수", "영희"};
assertThat(actualList).contains("철수", "영희");
contains() : 해당 컬렉션에 특정 요소가 포함되어 있는지의 여부를 검증
컬렉션 비교
assertThat(actualList).containsExactly(expectedArray);
assertThat(actualList).containsAnyOf(expectedArray);
containsExactly() : 순서까지 정확히 일치
containsAnyOf() : 요소 중 일부 포함
비어있는 컬렉션 검증
assertThat(actualList).isEmpty(); // 비어있는지 여부 검증
필드 및 속성 추출
List<User> users = Arrays.asList(
new User("재휘", 25),
new User("철수", 30),
new User("영희", 30)
);
assertThat(users)
.extracting("name")
.contains("영희", "철수");
extracting() : 객체나 컬렉션에서 특정 필드나 속성 값을 추출하여 검증할 때 사용.
객체 비교
assertThat(actual).isSameAs(expected);
assertThat(actual).isEqualToComparingFieldByField(expected);
isSameAs() : 참조값 비교
EqualToComparingFieldByField() : 필드 값 비교
객체의 타입 검증
assertThat(actual).isInstanceOf(ExpectedClass.class);
해당 Actual이 예상한 클래스의 인스턴스인지를 확인한다.
예외 검증
assertThatThrownBy(() -> someMethod()).isInstanceOf(SomeException.class);
assertThatThrownBy() : 내부의 익명 함수(람다식)에서 해당 Excpetion이 발생하면 통과
References
https://mangkyu.tistory.com/143
'Backend > Spring' 카테고리의 다른 글
[Spring Boot] 좋은 테스트 코드 : 필요성 및 실천 방법론 개요 (1) | 2024.07.22 |
---|---|
[Spring Boot] 테스트 : Test Double : 모듈 의존성 격리(Mocking) (0) | 2024.05.16 |
[Spring Boot/JPA] QueryDSL : 동적 쿼리 작성하기 (2) | 2024.02.13 |
[Spring Boot/JPA] Entity > DTO 변환 방법들 및 QueryDSL 프로젝션 (1) | 2024.02.13 |
[Spring Boot/JPA] QueryDSL 문법(3) : 서브쿼리, 상수/문자열 조작 (1) | 2024.02.01 |