TDD(Test-Driven Development)
테스트 주도 개발
반복 테스트를 이용한 소프트웨어 방법론, 작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 추가하는 단계를 반복하여 구현
짧은 개발 주기의 반복에 의존하는 개발 프로세스이며, 애자일 방법론 중 하나인 eXtream Programming(XP)의 'Test-First' 개념에 기반을 둔 단순한 설계를 중요시 함
eXtream Programming(XP)
미래에 대한 예측을 최대한 하지 않고, 지속적으로 프로토타입을 오나선하는 애자일 방법론 중 하나이다.
이 방법론은 추가 요구사항이 생기더라도, 실시간으로 반영할 수 있다.
TDD(Test-Driven Development), 테스트 주도 개발에 대한 프로그래머들의 의견은 늘 엇갈림
TDD의 실효성을 업무로 경험한 사람들은 TDD를 더 효과적으로 실무에 적용하기 위해 고민함
회사마다 일하는 방식이나 처한 업무 환경에 편차가 있다보니 일각에서는 실무에서 TDD를 사용하는 건 사실상 현실과 크다는 의견도 있음
TDD 개발주기
Red 단계에서는 실패하는 테스트 코드를 먼저 작성한다.
Green 단계에서는 테스트 코드를 성공시키기 위한 실제 코드를 작성한다.
Blue 단계에서는 중복 코드 제거, 일반화 등의 리팩토링을 수행한다.
중요한 것은 실패하는 테스트 코드를 작성할 때까지 실제 코드를 작성하지 않는 것과, 실패하는 테스트를 통과할 정도의 최소 실제 코드를 작성해야 하는 것
이를 통해, 실제 코드에 대해 기대되는 바를 보다 명확하게 정의함으로써 불필요한 설계를 피할 수 있고, 정확한 요구 사항에 집중할 수 있다.
TDD를 적용한 사례
:생년월일(input)을 입력받으면 현재 나이(output)를 출력하는 프로그램
1) 처음에는 간단한 것으로 목표를 정한다. (태어난 해와 올해의 연도를 입력해서 연도 뺄셈을 통해 나이 계산)
- 2015, 2018 -> (만)3살 우선 이것을 만들겠다는 생각을 한다.
2) 만들기도 전에 만든 후에 무엇을 테스트할지를 설계한다. (실패하는 테스트)
- 2015, 2018를 입력하면 2가 나오는 테스트 프로그램(장차 만들 프로그램을 테스트할 코드)를 만든다.
3) 그 다음에 그 테스트를 통과할 프로그램(1.을 목표로 작성한 코드)를 만든다.
- 2018 - 2015 (올해의 연도 - 태어난 해)
4) 테스트 프로그램으로 이 프로그램(3.에 해당하는 코드)을 실행한다.
5) 통과했으면 새로운 테스트를 추가한다.
- 이번에는 생월을 추가했을 때 계산하는 프로그램
6)위와 같은 작업을 계속 왔다갔다 수행한다.
일반 개발 방식 vs TDD 개발 방식
보통의 개발 방식은 '요구사항 분석 → 설계 → 개발 → 테스트 → 배포'의 형태로 개발 주기를 가짐, 이러한 방식은 소프트웨어 개발을 느리게 하는 잠재적 위험이 존재
이유
- 소비자의 요구사항이 처음부터 명확하지 않을 수 있다.
- 따라서 처음부터 완벽한 설계는 어렵다.
- 자체 버그 검출 능력 저하 또는 소스코드의 품질이 저하될 수 있다.
- 자체 테스트 비용이 증가할 수 있다.
문제점 발생 요인으로는 어떤 프로젝트든 초기 설계가 완벽하다고 말할 수 없기 때문
보통 고객의 요구사항 또는 디자인의 오류 등 많은 외부 또는 내부 조건에 의해, 재설계하여 점진적으로 완벽한 설계로 나아간다.
재설계로 인해 개발자는 코드를 삽입, 수정, 삭제하는 과정에서 불필요한 코드가 남거나 중복처리 될 가능성이 큼
결론적으로 이러한 코드들은 재사용이 어렵고 관리가 어려워져 유지보수를 어렵게 만든다.
작은 부분의 기능 수정에도 모든 부분을 테스트해야 하므로 전체적인 버그를 검출하기 어려워진다.
따라서 자체 버그 검출 능력이 저하되고, 어디서 버그가 발생할지 모르기 때문에 잘못된 코드도 고치지 않으려 하는 현상이 나타나고 이 현상은 소스코드의 품질 저하와 직결 됨.
작은 수정에도 모든 기능을 다시 테스트해야 하는 문제는 자체 테스트 비용 증가로 이어진다.
TDD와 일반적인 개발 방식의 가장 큰 차이점은 테스트 코드를 작성한 뒤에 실제 코드를 작성한다는 점
디자인 (설계) 단계에서 프로그래밍 목적을 반드시 미리 정의해야만 하고, 또 무엇을 테스트해야 할지 미리 정의(테스트 케이스 작성)해야만 한다.
- 테스트 코드를 작성하는 도중에 발생하는 예외 사항(버그, 수정사항)들은 테스트 케이스에 추가하고 설계를 개선한다.
- 이후 테스트가 통과된 코드만을 코드 개발 단계에서 실제 코드로 작성한다.
이러한 반복적인 단계가 진행되면서 자연스럽게 코드의 버그가 줄어들고, 소스코드는 간결해짐, 또한 테스트 케이스 작성으로 인해 자연스럽게 설계가 개선됨으로 재설계 시간이 절감된다.
TDD의 대표적인 Tool "JUnit"
JUnit은 현재 전 세계적으로 가장 널리 사용되는 Java 단위 테스트 프레임워크임
JUnit을 시작으로 CUnit, CppUnit, PyUnit 등 xUnit 프레임워크가 탄생하게 됨
TDD의 효과
1. 디버깅 시간을 단축할 수 있다.
유닛 테스팅을 하는 이점.
예를 들어 사용자의 데이터가 잘못 나오면 DB의 문제인지, 비즈니스 레이어의 문제인지 UI의 문제인지 실제 모든 레이어들을 전부 디버깅해야 하지만, TDD의 경우 자동화 된 유닛테스팅을 전제하므로 특정 버그를 손 쉽게 찾아낼 수 있다.
2. 코드가 내 손을 벗어나기 전에 가장 빠르게 피드백 받을 수 있다.
개발 프로세스에서는 보통 '인수 테스트'를 함(이미 배치된 시스템을 대상으로 클라이언트가 의뢰한 소프트웨어가 사용자 관점에서 사용할 수 있는 수준인지 체크하는 과정
이미 90% 이상 완성된 코드를 가지고 테스트하기 때문에, 문제를 발견해도, 정확하게 원인이 무엇인지 진단하기는 힘들다.
TDD를 사용하면 기능 단위로 테스트를 진행하기 때문에 코드가 모두 완성되어 프로그래머의 손을 떠나기 전에 피드백을 받는 것이 가능함
3. 작성한 코드가 가지는 불안정성을 개선하여 생산성을 높일 수 있다.
TDD를 사용하면, 코드가 내 손을 떠나 사용자에게 도달하기 전에 문제가 없는지 먼저 진단받을 수 있다. 그러므로 코드가 지닌 불안정성과 불확실성을 지속적으로 해소해 준다.
4. 재설계 시간을 단축할 수 있다.
테스트 코드를 먼저 작성하기 때문에 개발자가 지금 무엇을 해야 하는지 분명히 정의하고 개발을 시작하게 된다.
테스트 시나리오를 작성하면서 다양한 예외사항에 대해 생각해 볼 수 있음, 이는 개발 진행 중 소프트웨어의 전반적인 설계가 변경되는 일을 방지할 수 있다.
5. 추가 구현이 용이하다.
개발이 완료된 소프트웨어에 어떤 기능을 추가할 때 가장 우려되는 점은 해당 기능이 기존 코드에 어떤 영향을 미칠지 알지 못한다는 것임
TDD의 경우 자동화된 유닛 테스팅을 전제하므로 테스트 기간을 획기적으로 단축시킬 수 있다.
TDD의 단점
1. 생산성의 저하(가장 큰 단점)
개발 속도가 느려진다고 생각하는 사람이 많기 때문에 TDD에 대해 반신반의 한다.
처음부터 2개의 코드를 작성해야 하고, 중간중간 테스트를 하면서 고쳐나가야 하기 때문, TDD 방식의 개발 시간은 일반적인 개발 방식에 비해 대략 10~30% 정도로 늘어남
SI 프로젝트에서는 소프트웨어의 품질보다 납기일 준수가 훨씬 중요하기 때문에 TDD 방식을 잘 사용하지 않음
2. 개발하던 방식을 많이 바꿔야 한다.
몸에 체득한 것이 많을수록 바꾸기가 어렵다. 개발을 별로 해보지 않은 사람들에겐 적용하기가 쉽다.
3. 구조에 얽매인다.
원칙을 깰 수는 없고 꼼수가 있지만 그 꼼수를 위해서 구조를 변경하자니 아닌 것 같고, 테스트는 말 그대로 테스트일 뿐 실제 코드가 더 중요한 상황인데도 불구하고 테스트 원칙 때문에 쉽게 넘어가지 못하는 경우가 발생함.
JUnit 활용 Spring 프로젝트 테스트 코드 적용
필요 라이브러리
- JUnit : 자바 단위 테스트를 위한 테스팅 프레임워크
- AssertJ : 자바 테스트를 돕기 위해 다양한 문법을 지원하는 라이브러리
단위 테스트
- 코드의 특정 모듈이 의도된 대로 동작하는지 테스트하는 절차
- 모든 함수와 메서드에 대한 각각의 테스트 케이스를 작성하는 것
FIRST 원칙
- Fast : 테스트는 빠르게 동작해 자주 돌릴 수 있어야 함
- 테스트가 느리면 개발자가 테스트를 주저하게 될 것
- 자주 검증하지 않은 소스코드는 버그가 발생할 확률이 높아진다
- Independent : 독립적인 테스트가 가능해야 함
- 테스트에 필요한 데이터는 테스트 내부에서 독립적으로 사용해야 함
- 데이터 존재 여부를 찾는 테스트가 있는 경우에는 해당 데이터는 테스트 내부에서 생성되어야 하며, 나중에 다른 곳에 영향을 미치지 않게 제거되어야 함
- Repeatable : 어느 환경에서도 반복 가능해야 하고, 매번 같은 결과를 만들어야 함
- 네트워크나 데이터베이스에 의존하지 않는 환경
- 환경에 의존하지 않는 테스트가 실패할 수 있는 이유는 오로지 테스트할 클래스 또는 메서도가 제대로 작동하지 않기 때문
- Self-Validation : 테스트는 그 자체로 실행하여 결과 확인 가능해야 함
- 테스트가 실행될 때마다 메서드 출력이 올바른지 확인하는 것은 개발자가 결정해서는 안 됨
- Timely : 비즈니스 코드가 완성되기 전에 구성하고 테스트가 가능해야 함
JUnit
- 단위 테스트(Unit Test)를 위한 도구 제공
- 어노테이션 기반 테스트 지원
- 단정문(Assert)으로 테스트 케이스의 기대값에 대해 수행결과 확인 가능
- Spring Boot 2.2 버전부터 JUnit 5버전 사용
- JUnit5는 크게, Jupiter, Platform, Vintage 모듈로 구성됨
모듈 설명
JUnit Jupiter
- TestEngine API 구현체로 JUnit 5 구현하고 있음
- 테스트의 실제 구현체는 별도 모듈 역할을 수행하는데, 그 모듈 중 하나가 Jupiter-Engine
- 이 모듈은 jupiter-API를 사용해 작성한 테스트 코드를 발견하고 실행하는 역할을 수행
- 개발자가 테스트 코드 작성할 때 사용됨
JUnit Platform
- Test 실행하기 위한 뼈대
- Test를 발견하고 테스트 계획을 생성하는 TestEngine 인터페이스 갖고 있음
- TestEngine을 통해 Test 발견하고 수행 및 결과 보고
JUnit Vintage
- TestEngine API 구현체로 JUnit 3, 4를 구현하고 있음
- 기존 JUnit 3, 4 버전으로 테스트 코드를 실행할 때 사용됨
- Vintage-Engine 모듈 포함하고 있음
JUnit LifeCycle Annotation
JUnit 5 기준
JUnit Main Annotation
@SpringBootTest
- 통합 테스트 용도
- @SpringBootApplication을 찾아가 하위의 모든 Bean 스캔 해 로드함
- 그 후 Test용 Application Context를 만들어 Bean을 추가하고, 만약 MockBean으로 추가된 게 있다면 해당 Bean을 MockBean으로 교체
@ExtendWith
- JUnit4에서 @RunWith로 사용되던 어노테이션이 ExtendWith로 변경 됨
- @ExtendWith는 메인으로 실행될 Class를 지정할 수 있음
- Mockito의 Mock 객체를 사용하기 위한 Annotation
- @SpringBootTest는 기본적으로 @ExtendWith가 추가되어 있음
@WebMvcTest(Class명.class)
- ( )에 작성된 클래스만 실제로 로드하여 테스트 진행
- 매개변수를 지정하지 않으면 @Controller, @RestController, @RestControllerAdvice 등 컨트롤러와 연관된 Bean 모두 로드됨
- 스프링의 모든 Bean을 로드하는 @SpringBootTest 대신 컨트롤러 관련 코드만 테스트할 경우 사용
'@Autowired' about Mockbean
- Controller의 API를 테스트하는 용도인 MockMvc 객체를 주입받음
- MockMvc 객체 선언하고 그 위에 @Autowired 선언해야 함
- perform( ) 메서드를 활용해 컨트롤러의 동작 확인 가능
- andExpect( ), andDo( ), andReturn( ) 등 메서드를 같이 활용함
@MockBean
- 테스트할 클래스에서 주입 받고 있는 객체에 대해 가짜 객체를 생성해주는 어노테이션
- 해당 객체는 실제 행위를 하지 않음
- given( )을 활용해 가짜 객체의 동작에 대해 정의하여 사용가능
@AutoConfigureMockMvc
- spring.test.mockmvc의 설정을 로드하면서 MockMvc의 의존성 자동주입
- MockMvc 클래스는 REST API 테스트를 할 수 있는 클래스
@Import
- 필요한 Class들을 Configuration으로 만들어 사용 가능
- Configuration Component 클래스도 의존성 설정 가능
- Import 된 클래스는 주입으로 사용 가능
Spring Boot 프로젝트의 테스트 코드
단위(unit) 테스트: 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위 테스트 -> 쉽게 말하면 하나의 기능 혹은 메서드라고 이해하면 됩니다.
통합(integration) 테스트: 모듈을 통합화는 과정에서 모듈 간의 호환성을 확인하는 테스트 -> unit이 하나였다면 반대로 여러 개의 계층이 테스트에 참여한 것이라고 생각하면 쉬울 거 같습니다.
단위 테스트 장단점
장점
- 새로운 기능에 대해서 빠르게 작성 가능
- Test 코드 자체가 하나의 문서
- 시간과 비용의 절감
단점
- 독립적인 테스트이므로 다른 객체와 상호작용 처리를 위해서 가짜 객체 정의 필요함
- 가짜 객체의 답변 작성 필요함
- 실제 운영 환경과 다른 답변을 내놓을 수 있는 가능성이 있음
통합 테스트 장단점
장점
- 실제 객체를 사용하므로 가짜 객체 사용하지 않아 정의하지 않아도 됨
- 실제 운영 환경과 같은 값을 도출 가능함
단점
- 테스트 하나에 많은 비용이 들어감
- 어느 계층에서 발생한 문제인지 파악하기 힘듦
좋은 단위 테스트
- 1개의 테스트는 1개의 기능에 대해서만 테스트
- 테스트 주체와 협력자를 구분하기(주체 - 테스트를 할 객체, 협력자 - 테스트를 진행하기 위해 정의하는 가짜 객체)
- Given, when, then으로 명확학 ㅔ작성하기
- Given : 테스트를 진행할 행위를 위한 사전 준비
- when : 테스트를 진행할 행위
- then : 테스트를 진행한 행위에 대한 결과 검증
Springboot Test 코드(단위 테스트, 통합 테스트)
1. Member를 만들 수 있습니다.
2. Member는 이름과 나이를 가지고 Name은 중복이 불가능합니다.
3. Member는 나이를 변경할 수 있습니다.
4. Member의 리스트를 받을 수 있습니다.
1. Member
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "age")
private int age;
@Builder
public Member(String name, int age){
this.name=name;
this.age=age;
}
@Override
public boolean equals(Object obj) { // 협력자에서 이름 중복 예외를 검증하기 위함
Member me = (Member) obj;
return this.name.equals(me.name) && this.age==me.age;
}
public void changeAge(int age){
this.age=age;
}
}
2. MemberRepository
public interface MemberRepository extends JpaRepository<Member,Long> {
Optional<Member> findByName(String name);
}
3. MemberService, MemberServiceImpl
public interface MemberService {
List<MemberResponseDto.ListDto> findAll();
Long createMember(String name, int age);
}
@Service
@Transactional
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
@Override
public List<MemberResponseDto.ListDto> findAll(){
return memberRepository.findAll().stream().map(a -> new MemberResponseDto.ListDto(a.getName(),a.getAge()))
.collect(Collectors.toList());
}
@Override
public Long createMember(String name, int age){
memberRepository.findByName(name).ifPresent(a -> {
throw new IllegalStateException("이미 있는 아이디");
});
Member member = Member.builder()
.age(age)
.name(name).build();
return memberRepository.save(member).getId();
}
}
4. MemberController, MemberControllerAdvice
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
@GetMapping("/members")
public List<MemberResponseDto.ListDto> getMemberList(){
return memberService.findAll();
}
@PostMapping("/members")
public Long createMember(@RequestBody MemberRequestDto.CreateDto createDto){
return memberService.createMember(createDto.getName(), createDto.getAge());
}
}
@RestControllerAdvice(basePackages = "com.example.fortest.domain.member.controller")
public class MemberControllerAdvice {
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String duplicate(IllegalStateException e){
return e.getMessage();
}
}
5. MemberDto
public class MemberRequestDto {
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class CreateDto {
String name;
int age;
}
}
public class MemberResponseDto {
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class ListDto {
String name;
int age;
}
}
Test Code 작성
AssertJ 라이브러리
Assertions는 assertJ 라이브러리를 사용
1. assertThat은 값 검증에 사용 됨
- assertThat(실제값).isEqualTo(기대값)
- assertThat(실제 객체).isInstanceOf(객체 예상 타입)
- assertThat(실제 값).isNull()
2. assertThatThrowBy는 예외 발생 검증에 사용
- asserThatThrownBy( () -> 예외를 발생시킬 로직). isInstanceOf(예외 클래스)
- 예외가 발생한다면 테스트를 통과하고 발생하지 않는다면 실패하는 메서드
Domain Test
가장 단위가 작은 Member 객체에 대해서 단위 테스트, 도메인에 대한 테스트가 가장 비용이 적게 듦.
@Test 어노테이션이 반드시 필요하며, 반환하는 것이 없도록 void 타입으로 설정
@DisplayName을 통해서 테스트 진행 시 나오는 테스트명을 정할 수 있음
@Test
@DisplayName("멤버가 생성되는지 확인하는 테스트")
void createMember(){
/*
given
*/
Member member = Member.builder().age(10).name("hi").build();
/*
when, then
*/
Assertions.assertThat(member.getAge()).isEqualTo(10);
Assertions.assertThat(member.getName()).isEqualTo("hi");
}
- Builder를 이용해서 멤버를 생성했을 때 올바르게 생성됐는지 테스트
@Test
@DisplayName("멤버의 나이 바뀌는지 확인하는 테스트")
void changeAgeTest(){
/*
given
*/
Member member = Member.builder().age(10).name("hi").build();
/*
when
*/
member.changeAge(13);
/*
then
*/
Assertions.assertThat(member.getAge()).isEqualTo(13);
}
- Member의 나이를 바꿨을 때 올바르게 바뀌는지 테스트
@Test
@DisplayName("멤버의 나이 바뀌는지 확인하는 테스트")
void changeAgeTest(){
/*
given
*/
Member member = Member.builder().age(10).name("hi").build();
/*
when
*/
member.changeAge(13);
/*
then
*/
assertThat(member.getAge()).isEqualTo(12);
}
- 나이를 바꾸고, 검증이 다르게 나와 Test가 실패했을 때 실패 이유를 알 수 있음
- 모두 독립적으로 실행되기에 같이 돌려도 결과에 미치는 영향은 없음
Jpa를 사용하는 Repsitory Test
@DataJpaTest: Jpa를 사용하는 Repository에 대한 검증을 수행할 때 사용하는 어노테이션
@DataJpaTest는 @Transaction을 포함하고 있어서 1개 의 테스트가 끝나면 Rollback 해 다른 테스트에게 영향을 미치지 않음
@DataJpaTest로 검증할 수 있는 목록
- DataSource에 대한 설정
- CRUD가 제대로 동작하는지
@AutoConfigurationDatabase에 Replace.NONE설정을 주면 실제 DB로 검증할 수 있음, 따로 명시하지 않을 시 내장된 임베디드 DB 사용
@DataJpaTest
public class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Test
@DisplayName("멤버 만들기")
void createMember(){
/*
given
*/
Member member1 = Member.builder().name("hi1").age(10).build();
Member member2 = Member.builder().name("hi2").age(20).build();
/*
when
*/
Member result1 = memberRepository.save(member1);
Member result2 = memberRepository.save(member2);
/*
then
*/
assertThat(result1.getAge()).isEqualTo(member1.getAge());
assertThat(result2.getAge()).isEqualTo(member2.getAge());
}
@Test
@DisplayName("멤버의 리스트를 반환 하는지 확인")
void MemberList(){
/*
given
*/
Member member1 = Member.builder().name("hi1").age(10).build();
Member member2 = Member.builder().name("hi2").age(20).build();
memberRepository.save(member1);
memberRepository.save(member2);
/*
when
*/
List<Member> result = memberRepository.findAll();
/*
then
*/
assertThat(result.size()).isEqualTo(2);
}
}
- Member를 저장이 잘 됐는지 검증
- Member를 저장하고, Member List를 잘 반환하는지 검증
Service 계층 Test
Service 계층은 Repository객체를 Spring에게 주입받음
Service계층의 Test는 주체가 Service 객체이며, 협력자는 Repository객체
Repository는 가짜 객체로서 응답을 설정
Junit5 기능을 사용하고, Test에서 가짜 객체를 사용하기 때문에 @ExtendWith(SpringExtension.class)를 붙여줘야 함
@ExtendWith(SpringExtension.class)
public class MemberServiceTest {
// Test 주체
MemberService memberService;
// Test 협력자
@MockBean
MemberRepository memberRepository;
// Test를 실행하기 전마다 MemberService에 가짜 객체를 주입시켜준다.
@BeforeEach
void setUp(){
memberService = new MemberServiceImpl(memberRepository);
}
}
- @BeforeEach: Test를 실행하기 전 항상 실행하도록 하는 어노테이션입니다. 여기서는 가짜 객체를 주입하는 데 사용
- @MockBean: 가짜 객체를 만드는 역할을 합니다. 물론 가짜 객체이므로 응답을 정의해줘야 함, Test의 협력자 역할
- MemberService: Test의 주체로서 가짜 객체를 주입받고, 자신의 로직을 실행하고 결과를 가지고 검증
@Test
@DisplayName("멤버 생성 성공")
void createMemberSuccess(){
/*
given
*/
Member member3 = Member.builder().name("hi3").age(10).build();
ReflectionTestUtils.setField(member3,"id",3l);
Mockito.when(memberRepository.save(member3)).thenReturn(member3); // 가짜 객체 응답 정의
/*
when
*/
Long hi3 = memberService.createMember("hi3", 10);
/*
then
*/
assertThat(hi3).isEqualTo(3L);
}
- Member 생성을 성공하는 Test
- ReflectionTestUtils.setField() : test를 진행하면서 private로 선언된 필드 값을 넣어줄 수 있음
- Mockito.when(가짜 객체의 로직 실행). thenReturn(실행되면 이것을 반환)
@Test
@DisplayName("멤버 생성시 member1 과 이름이 같아서 예외 발생")
void createMemberFail(){
/*
given
*/
Member member1 = Member.builder().name("hi1").age(10).build();
Mockito.when(memberRepository.findByName("hi1")).thenReturn(Optional.of(member1));
/*
when then
*/
assertThatThrownBy(() -> memberService.createMember("hi1",10)).isInstanceOf(IllegalStateException.class);
}
- assertThatThrownBy는 예외 발생을 검증하는 메서드
- memberRepository.findByName("hi1"))을 했을 경우 Optional.of(member1)이 반환
- MemberService내에는 name으로 중복 예외를 터트리는 로직이 있으므로 예외가 발생
- 따라서 예외가 발생해서 테스트를 통과하는 모습을 볼 수 있음
@Test
@DisplayName("멤버 생성시 member1 과 이름이 같아서 예외 발생")
void createMemberFail(){
/*
given
*/
Member member1 = Member.builder().name("hi1").age(10).build();
Mockito.when(memberRepository.findByName("hi1")).thenReturn(Optional.of(member1));
/*
when then
*/
assertThatThrownBy(() -> memberService.createMember("hi2",10)).isInstanceOf(IllegalStateException.class);
}
- 반대로 예외를 발생시키지 않는다면, 테스트를 실패
Controller 계층
@WebMvcTest: Mvc를 위한 테스트로서 컨트롤러가 설계대로 동작하는지에 대해 검증하는데 필요한 어노테이션
Controller를 구체적으로 적을 수 있고, ControllerAdvice, Filter 등을 포함과 제외시킬 수 있어 Security에 대한 Test도 가능
@WebMvcTest(MemberController.class)
public class MemberControllerTest {
@Autowired
MockMvc mvc;
@MockBean
MemberServiceImpl memberService;
}
- Test의 주체는 MemberController입니다. 따라서 WebMvcTest에 선언을 해줍니다
- MemberService는 협력자이므로 @MockBean을 등록해주고, Test에 응답을 정의
- MockMvc는 실제로 서블릿 컨테이너를 사용하지 않고, 테스트용으로 Mvc 기능을 사용할 수 있게 해주는 역할, 테스트 때 생성되는 WebApplicationContext에서 주입 받음
@Test
@DisplayName("리스트 반환받기")
void getList() throws Exception {
/*
given
*/
List<MemberResponseDto.ListDto> list = List.of(new MemberResponseDto.ListDto("asd", 10)
, new MemberResponseDto.ListDto("fsd", 12));
Mockito.when(memberService.findAll()).thenReturn(list);
/*
when then
*/
mvc.perform(MockMvcRequestBuilders.get("/members").contentType(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$[1].name").value("fsd"))
.andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("asd"));
}
- 협력자인 MemberService의 findAll() 호출 시 반환하는 list를 정의
- mvc.perform(MockMvcRequestBuilders.get(). contentType(): 컨트롤러에게 요청을 보내는 역할, uri를 만들고, contentType을 지정
- andDo(): 요청에 대한 처리를 합니다. MockMvcResultHanlder.print()를 인자로 넣었으므로 요청과 응답에 대한 것들을 콘솔에 출력
- andExpect(): 검증하는 로직입니다. MockMvcResultMatcher.status()는 HTTP 상태 코드를 검증하고, jsonPath는 Json로 넘어온 것들에 대한 값을 검증할 수 있음
- jsonPath("$. name"). value("fsd"): 단일 객체에 대한 값 검증
- jsonPath("$[1]. name"). value("asd): 리스트를 반환받았을 때 지정하여 검증
통합 테스트
단위 테스트와는 다르게 실제 spring을 실행했을 때와 같은 운영환경에서 잘 동작하는지 확인하는 테스트
전체적인 플로우를 확인하므로 Spring에 쓰이는 Bean들이 등록 됨
@SpringBootTest: 통합 테스트를 진행하기 위한 어노테이션(주의 @Transaction을 포함하고 있지 않기 때문에 Repository 계층까지 사용된다면 @Transaction도 붙여서 Rollback을 실행해줘야 함)
public class MemberServiceTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
}
- 단위 테스트보다 쉽게 진행할 수 있음, SpringContainer를 만들기 때문에 Bean 있어 따로 가짜 객체를 정의하지 않아도 됨
@Test
@DisplayName("멤버 만들기")
void createMemberSuccess(){
Long memberId = memberService.createMember("hi1", 10);
assertThat(memberId).isEqualTo(1l);
}
@Test
@DisplayName("이름 중복으로 만들기 실패")
void createMemberFail(){
Long memberId = memberService.createMember("hi1", 10);
assertThatThrownBy(() -> memberService.createMember("hi1",12)).isInstanceOf(IllegalStateException.class);
}
Test를 진행하는 데 걸리는 시간
프로젝트를 진행하며, 테스트 코드가 적다면, 통합 테스트로 진행해도 많은 영향을 끼치진 않지만 애플리케이션이 커지면서, 테스트 코드가 점점 많아진다면 비용이 많이 발생함
오류가 발생한 곳 색출
단위 테스트의 경우 계층마다 나눠서 작성하기 때문에 오류가 발생해서 검증이 실패한다면, 그 부분만 수정하면 됨, 반면에 통합 테스트는 어느 계층에서 오류가 발생한 지 색출하는데 오랜 시간이 걸림
'항해 99 > Spring' 카테고리의 다른 글
Controller, RestController 차이 (0) | 2024.03.05 |
---|---|
메서드 명 find와 get의 차이 (0) | 2024.03.04 |
Swagger 사용하기 (0) | 2024.03.01 |
Spring - JPA Entity 연관 관계 (1) | 2024.02.27 |
Spring - Bean, 로그인/회원가입, Security, Validation (1) | 2024.02.26 |