소프트웨어의 제품 or 서비스의 품질을 확인하거나, 소프트웨어의 버그를 찾을 때 작성하는 코드를 의미합니다.
다시 말해, 제품이 예상하는 대로 동작 하는지 확인하는 것!
Unit Test (단위 테스트)
함수나 모듈 클래스와 같은 단위 딱 하나를
테스트 하는 것을 의미합니다.
예를 들어, 자전거에서 바퀴 하나만을 테스트 하는 것 입니다.
Spring : 블로그 서비스에서, 게시글 저장 repository 코드 테스트
Integration Test (통합 테스트)
여러 가지 단위들을 통합시켰을 때,
서로 상호작용을 작 하는지 테스트하는 것 을 의미합니다.
예를 들어, 자전거에서 바퀴와 체인의 연결성을 테스트 하는 것 입니다.
Spring : 블로그 서비스에서, 게시글 저장에 대한 controller, service, repository 의 연결성 테스트
User Interface Test
실제 사용자가 앱을 사용했을 때의
사용자 흐름에 맞추어 테스트하는 것 을 의미합니다.
예를 들어, 자전거 탑승 부터, 핸들을 잡고, 페달을 밟아가며 전체적으로 테스트 하는 것 입니다.
Spring : 실제 REST API 호출을 하며 테스트 해 보는 것.
Test 코드 작성 시, 가장 추천받는 코딩 스타일 입니다.
given
when
then
즉, 어떤 상태에서 출발 (given)하여 해당 상태에 어떤 변화를 가했을 때 (when)
기대하는 어떠한 상태가 되어야 합니다. (then)
정의
TDD 가 필요한 상황
정의
특징
단정문 : 테스트의 성공과 실패를 판별하는 문장 입니다.
@Test // 테스트 메서드 임을 명시
@DisplayName("Assertion 이용해보기") // 테스트 이름 표시
void useAssertion() {
// assertTrue(조건식) - 다음 조건식이 true 인지 확인
Assertions.assertTrue(true);
// assertEquals(기대 값, 실제 값) - 실제 값이 기대 값과 같은지 확인
Assertions.assertEquals("expected", "expected");
// assertNotNull(값) - 해당 값이 null 이 아닌지 확인
Assertions.assertNotNull(new Object());
// assertThrows(기대하는 예외 타입, 실행 코드)
// - 실행 코드에서 해당 예외 타입이 발생하는지 확인
Assertions.assertThrows(RuntimeException.class, () -> makeRuntimeException());
// assertTimeOut(끝나야하는 시간, 실행 코드) - 실행 코드가 끝나야 한느 시간 안에 끝나는지 확인
// 10초 안에 끝나는지 확인
Assertions.assertTimeout(Duration.ofSeconds(10), () -> timeOutMethod());
// assertAll(한번에 확인하고 싶은 모든 assertion)
Assertions.assertAll(
() -> Assertions.assertTrue(true),
() -> Assertions.assertTrue(true),
() -> Assertions.assertTrue(true)
);
}
@Test
메서드 위에 해당 Annotation 을 선언해, 테스트 대상 메서드임을 지정할 수 있습니다.###
@BeforeEach
모든 @Test 메서드가 실행되기 전에 실행되는 메서드를 지정하는 Annotation 입니다.
테스트 마다 공통으로 쓰이면서, 테스트 전에 초기화되어야 할 항목이 들어갑니다.
@AfterEach
모든 @Test 메서드의 실행이 끝난 뒤에 실행되는 메서드를 지정하는 Annotation 입니다.
각 테스트가 끝나고 각각 호출됩니다.
@BeforeAll
해당 테스트 클래스가 실행될 때, 딱 한번만 수행되는 메서드를 지정하는 Annotation 입니다.
ex) DB 연결
Spring 프로젝트 생성 시, src/test 폴더에 Test 코드를 작성하면 됩니다.
기본적으로 메인 클래스 이름 + Tests 가 붙은 Test 용 클래스가 제공됩니다.
@SpringBootTest
애플리케이션 전체 (스프링 컨테이너에 등록될 모든 비)를 로드하여 테스트 진행 합니다. - 통합 테스트
@SpringBootTest
class BlogApplicationTests {
@Test // 테스트를 위한 메서드임을 명시해줍니다.
void contextLoads() {
}
}
@DataJpaTest
@AutoConfigureTestDatabase
@Transactional
test.java.gdsc.blog.unit.repository.PostRepositoryUnitTest.java
// @Transactional - @DataJpaTest 에 포함되어 있습니다.
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
@DataJpaTest
public class PostRepositoryUnitTest {
@Autowired // 필요한 Bean 에 대하여 필드 주입 방식으로 의존성 주입
private PostRepository postRepository;
@Autowired
private EntityManager entityManager;
@BeforeEach
public void init() {
// 테이블 autoincrement 초기화
// @Transactional 로 트랜잭션이 롤백되어도, AutoIncrement로 설정된 숫자 정책은 초기화 되지 않음
entityManager
.createNativeQuery("ALTER TABLE post ALTER COLUMN id RESTART WITH 1")
.executeUpdate();
}
}
...
public class PostRepositoryUnitTest {
...
@Test
public void save_테스트() {
// given - 주어진 상황 (Post 데이터)
Post post = new Post(null, "스프링부트 따라하기", "스프링부트 따라하기 내용");
// when - 특정 행동 (Post 데이터 저장)
Post postEntity = postRepository.save(post);
// then - 결과 (저장 결과의 제목 확인)
assertEquals("스프링부트 따라하기", postEntity.getTitle());
}
}
...
public class PostRepositoryUnitTest {
...
@Test
public void saveAll_테스트() {
// given
List<Post> postList = Arrays.asList(
new Post(1L, "스프링부트 따라하기", "스프링부트 따라하기 내용"),
new Post(2L, "리엑트 따라하기", "리엑트 따라하기 내용")
);
// when
List<Post> postEntityList = postRepository.saveAll(postList);
// then
assertArrayEquals(postEntityList.toArray(), postList.toArray());
}
...
}
...
public class PostRepositoryUnitTest {
...
@Test
public void findById_테스트() {
// given
postRepository.saveAll(
Arrays.asList(
new Post(1L, "스프링부트 따라하기", "스프링부트 따라하기 내용"),
new Post(2L, "리엑트 따라하기", "리엑트 따라하기 내용")
)
);
Long id = 1L;
// when
Optional<Post> postEntity = postRepository.findById(id);
// then
assertTrue(postEntity.isPresent());
assertEquals(1L, postEntity.get().getId());
assertEquals("스프링부트 따라하기", postEntity.get().getTitle());
}
...
}
...
public class PostRepositoryUnitTest {
...
@Test
public void findById_empty_테스트() {
// given
Long id = 1L;
// when
Optional<Post> postEntity = postRepository.findById(id);
// then
assertTrue(postEntity.isEmpty());
}
...
}
...
public class PostRepositoryUnitTest {
...
@Test
public void findAll_테스트() {
// given
postRepository.saveAll(
Arrays.asList(
new Post(null, "스프링부트 따라하기", "스프링부트 따라하기 내용"),
new Post(null, "리엑트 따라하기", "리엑트 따라하기 내용")
)
);
// when
List<Post> postEntityList = postRepository.findAll();
// then
assertNotEquals(0, postEntityList.size());
assertEquals(2, postEntityList.size());
}
...
}
...
public class PostRepositoryUnitTest {
...
public void deleteById_테스트() {
// given
postRepository.saveAll(
Arrays.asList(
new Post(1L, "스프링부트 따라하기", "스프링부트 따라하기 내용"),
new Post(2L, "리엑트 따라하기", "리엑트 따라하기 내용")
)
);
Long id = 1L;
// when
postRepository.deleteById(id);
Optional<Post> postEntity = postRepository.findById(id);
// then
assertTrue(postEntity.isEmpty());
}
...
}
Mockito 환경을 사용하는 이유
@ExtendWith(MockitoExtension.class)
클래스 상단에 추가하여, 해당 JUnit 테스트 클래스가 Mockito 환경으로 실행되는 것 을 명시합니다.
@Mock
단위 테스트를 위한 가짜 객체를 생성하여 Mock 환경에 띄워줍니다.
@InjectMocks
@Mock 또는 @Spy 로 생성된 Mock 객체들의 의존성을 주입합니다.
@InjectMocks을 쓴 객체의 Mock 객체를 주입 받을 수 있는 형태 : 생성자, 수정자, 필드 주입 방식
test.java.gdsc.blog.unit.service.PostServiceUnitTest.java
@ExtendWith(MockitoExtension.class) // Mockito 환경으로 실행
public class PostServiceUnitTest {
@InjectMocks // (PostService 객체가 만들어질때) 해당 파일에 @Mock로 등록된 모든 애들을 주입 받는다.
private PostService postService;
// PostRepository => 가짜 객체로 만들 수 있음 - Mockito 환경에서 이를 제공
@Mock
private PostRepository postRepository;
}
@ExtendWith(MockitoExtension.class)
public class PostServiceUnitTest {
...
@Test
public void save_테스트() {
// given
Post post = new Post();
post.setTitle("스프링부트 따라하기");
post.setContent("스프링부트 따라하기 내용");
// stub - 동작 지정
when(postRepository.save(post)).thenReturn(post);
// test execute
Post postEntity = postService.save(WritePostReq.builder()
.title("스프링부트 따라하기")
.content("스프링부트 따라하기 내용")
.build());
// then
assertEquals(post, postEntity); // expected , actual
}
}
@ExtendWith(MockitoExtension.class)
public class PostServiceUnitTest {
...
@Test
public void findById_테스트() {
// given
Long id = 1L;
Post post = new Post();
post.setId(id);
post.setTitle("스프링부트 따라하기");
post.setContent("스프링부트 따라하기 내용");
// stub - 동작 지정
when(postRepository.findById(id)).thenReturn(java.util.Optional.of(post));
// when
Post postEntity = postService.findById(id);
// then
assertEquals(post, postEntity);
}
}
@ExtendWith(MockitoExtension.class)
public class PostServiceUnitTest {
...
@Test
public void findById_fail_테스트() {
// given
Long id = 1L;
// stub - 동작 지정
when(postRepository.findById(id)).thenReturn(Optional.empty());
// when & then
// assertThrows 에서 해당 실행 부분이 expected Exception 을 throw 하는지 확인
Exception exception = assertThrows(NoSuchElementException.class, () -> {
postService.findById(id);
});
assertEquals("id를 확인해주세요!!", exception.getMessage());
}
}
@ExtendWith(MockitoExtension.class)
public class PostServiceUnitTest {
...
@Test
public void findAll_테스트() {
// given
List<Post> postList = new ArrayList<>();
postList.add(new Post(1L, "스프링부트 따라하기", "스프링부트 따라하기 내용"));
postList.add(new Post(2L, "리액트 따라하기", "리액트 따라하기 내용"));
// stub - 동작 지정
when(postRepository.findAll()).thenReturn(postList);
// when
List<Post> postEntityList = postService.findAll();
// then
assertEquals(postList, postEntityList);
}
}
@ExtendWith(MockitoExtension.class)
public class PostServiceUnitTest {
...
@Test
public void updateById_테스트() {
// given
Long id = 1L;
Post post = new Post();
post.setId(id);
post.setTitle("스프링부트 따라하기");
post.setContent("스프링부트 따라하기 내용");
WritePostReq writePostReq = WritePostReq.builder()
.title("스프링부트 또 따라하기")
.content("스프링부트 또 따라하기 내용").build();
// stub - 동작 지정
when(postRepository.findById(1L)).thenReturn(java.util.Optional.of(post));
// when
Post postEntity = postService.updateById(id, writePostReq);
// then
assertEquals("스프링부트 또 따라하기", postEntity.getTitle());
assertEquals("스프링부트 또 따라하기 내용", postEntity.getContent());
}
}
@WebMvcTest
Web Layer 를 테스트 하고 싶을 때 사용한다. 즉, Controller 관련 로직만 메모리에 띄웁니다.
@ExtendWith(SpringExtension.class)
클래스 상단에 추가하여, 해당 JUnit 테스트 클래스가 Spring 환경으로 실행되는 것 을 명시 합니다.
test.java.gdsc.blog.unit.controller.PostControllerUnitTest.java
// -> @ExtendWith(SpringExtension.class) - 스프링 환경 확장시 사용하는 애노테이션
// - Spring 에서 JUnit5 에서 테스트 할때 필수
@WebMvcTest // @ExtendWith(SpringExtenstion.class) 를 포함
public class PostControllerUnitTest {
@Autowired
private MockMvc mockMvc;
@MockBean // IoC 환경에 해당 가짜 빈이 등록된다.
// - Spring 환격을 사용하였기 때문에, 빈으로 등록해주어야 한다.
private PostService postService;
}
@WebMvcTest
public class PostControllerUnitTest {
...
@Test // 테스트 명시
public void save_테스트() throws Exception {
// given (테스트를 하기 위한 준비)
WritePostReq writePostReq = WritePostReq.builder()
.title("스프링부트 따라하기")
.content("스프링부트 따라하기 내용")
.build();
// Object 를 JSON 으로 변경해주는 함수
String content = new ObjectMapper().writeValueAsString(writePostReq);
// stub (미리 행동을 지정함) - postService 는 가짜 이기 때문에 제대로 실행되지 않기 때문에 - controller 만 신경쓰기 때문에 가능
when(postService.저장하기(writePostReq)).thenReturn(new Post(1L, "스프링부트 따라하기", "스프링부트 따라하기 내용"));
// when (테스트 실행)
ResultActions resultAction = mockMvc.perform(post("/post")
.contentType(MediaType.APPLICATION_JSON)
.content(content)
.accept(MediaType.APPLICATION_JSON));
// then (검증)
resultAction
.andExpect(status().isCreated())
.andExpect(jsonPath("$.title").value("스프링부트 따라하기")) // jsonPath : json 에서 변수로 결과 받아옴
.andExpect(jsonPath("$.content").value("스프링부트 따라하기 내용")) // $ 는 전체를 뜻함, . 은 구분자
.andDo(MockMvcResultHandlers.print()); // 결과 출력
}
}
@WebMvcTest
public class PostControllerUnitTest {
...
@Test
public void findAll_테스트() throws Exception{
// given
// stub 생성
List<Post> postList = new ArrayList<>();
postList.add(new Post(1L, "스프링부트 따라하기", "스프링부트 따라하기 내용"));
postList.add(new Post(2L, "리액트 따라하기", "리액트 따라하기 내용"));
when(postService.모두가져오기()).thenReturn(postList);
// when
ResultActions resultActions = mockMvc.perform(get("/post")
.accept(MediaType.APPLICATION_JSON));
// then
resultActions
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.hasSize(2)))
.andExpect(jsonPath("$.[0].title").value("스프링부트 따라하기"))
.andDo(MockMvcResultHandlers.print());
}
}
@WebMvcTest
public class PostControllerUnitTest {
...
@Test
public void findById_테스트() throws Exception{
// given
Long id = 1L;
// stub 생성
when(postService.한건가져오기(id)).thenReturn(new Post(1L, "스프링부트 따라하기", "스프링부트 따라하기 내용"));
// when
ResultActions resultAction = mockMvc.perform(get("/post/{id}", id)
.accept(MediaType.APPLICATION_JSON));
// then
resultAction
.andExpect(status().isOk())
.andExpect(jsonPath("$.title").value("스프링부트 따라하기"))
.andDo(MockMvcResultHandlers.print());
}
}
@WebMvcTest
public class PostControllerUnitTest {
...
@Test
public void findById_fail_테스트() throws Exception {
// given
Long id = 1L;
when(postService.한건가져오기(id)).thenThrow(new NoSuchElementException("id를 확인해주세요!!"));
// when
ResultActions resultActions = mockMvc.perform(get("/post/{id}", id)
.accept(MediaType.APPLICATION_JSON));
// then
resultActions
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("$.code").value("Item Not Found"))
.andExpect(jsonPath("$.message").value("id를 확인해주세요!!"))
.andDo(MockMvcResultHandlers.print());
}
}
@WebMvcTest
public class PostControllerUnitTest {
...
@Test
public void updateById_테스트() throws Exception {
// given
Long id = 1L;
WritePostReq writePostReq = WritePostReq.builder()
.title("스프링부트 또 따라하기")
.content("스프링부트 또 따라하기 내용")
.build();
String content = new ObjectMapper().writeValueAsString(writePostReq);
// postService 는 가짜로 올라가있는 것 이기 때문에 실제 실행되는 것은 아님
when(postService.수정하기(id, writePostReq)).thenReturn(new Post(1L, "스프링부트 또 따라하기", "스프링부트 또 따라하기 내용"));
// when
ResultActions resultActions = mockMvc.perform(put("/post/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.content(content)
.accept(MediaType.APPLICATION_JSON));
// then
resultActions
.andExpect(status().isOk())
.andExpect(jsonPath("$.title").value("스프링부트 또 따라하기"))
.andDo(MockMvcResultHandlers.print());
}
}
@WebMvcTest
public class PostControllerUnitTest {
...
@Test
public void delete_테스트() throws Exception {
// given
Long id = 1L;
// postService 는 가짜로 올라가있는 것 이기 때문에 실제 실행되는 것은 아님
when(postService.삭제하기(id)).thenReturn("ok");
// when
ResultActions resultActions = mockMvc.perform(delete("/post/{id}", id)
.accept(MediaType.TEXT_PLAIN));
// then - JSON 응답 시
resultActions
.andExpect(status().isOk())
.andDo(MockMvcResultHandlers.print());
// 문자(String) 응답 시
MvcResult requestResult = resultActions.andReturn();
String result = requestResult.getResponse().getContentAsString();
assertEquals("ok", result);
}
}
test.java.gdsc.blog.integration.PostIntegration.java
@Transactional
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
// -> @ExtendWith(SpringExtension.class)
// - 스프링 환경 확장시 사용하는 애노테이션 - Spring 에서 JUnit5 에서 테스트 할때 필수
public class PostControllerIntegreTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private PostRepository postRepository;
@Autowired
private EntityManager entityManager;
@BeforeEach // 모든 테스트 이전에 실행 됨
public void init() {
entityManager
.createNativeQuery("ALTER TABLE post ALTER COLUMN id RESTART WITH 1")
.executeUpdate();
}
}
...
public class PostControllerIntegreTest {
...
@Test // 테스트 명시
public void save_테스트() throws Exception {
// given (테스트를 하기 위한 준비)
WritePostReq writePostReq = WritePostReq.builder()
.title("스프링부트 따라하기")
.content("스프링부트 따라하기 내용")
.build();
// Object 를 JSON 으로 변경해주는 함수
String content = new ObjectMapper().writeValueAsString(writePostReq);
// 실제 postService 가 Bean 으로 등록 되어 있기 때문에, stub 이 필요 없음
// when (테스트 실행)
ResultActions resultAction = mockMvc.perform(post("/post")
.contentType(MediaType.APPLICATION_JSON)
.content(content)
.accept(MediaType.APPLICATION_JSON));
// then (검증)
resultAction
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.title").value("스프링부트 따라하기"))
// jsonPath : json 에서 변수로 결과 받아옴
.andExpect(jsonPath("$.content").value("스프링부트 따라하기 내용"))
// $ 는 전체를 뜻함, . 은 구분자
.andDo(MockMvcResultHandlers.print()); // 결과 출력
}
}
...
public class PostControllerIntegreTest {
...
@Test
public void findAll_테스트() throws Exception{
// given
// data 생성
List<Post> postList = new ArrayList<>();
postList.add(new Post(null, "스프링부트 따라하기", "스프링부트 따라하기 내용"));
postList.add(new Post(null, "리액트 따라하기", "리액트 따라하기 내용"));
postRepository.saveAll(postList);
// when
ResultActions resultActions = mockMvc.perform(get("/post")
.accept(MediaType.APPLICATION_JSON));
// then
resultActions
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.hasSize(2)))
.andExpect(jsonPath("$.[0].id").value(1L))
.andExpect(jsonPath("$.[0].title").value("스프링부트 따라하기"))
.andDo(MockMvcResultHandlers.print());
}
}
...
public class PostControllerIntegreTest {
...
@Test
public void findById_테스트() throws Exception{
// given
// data 생성
List<Post> postList = new ArrayList<>();
postList.add(new Post(null, "스프링부트 따라하기", "스프링부트 따라하기 내용"));
postList.add(new Post(null, "리액트 따라하기", "리액트 따라하기 내용"));
postRepository.saveAll(postList);
Long id = 1L;
// when
ResultActions resultAction = mockMvc.perform(get("/post/{id}", id)
.accept(MediaType.APPLICATION_JSON));
// then
resultAction
.andExpect(status().isOk())
.andExpect(jsonPath("$.title").value("스프링부트 따라하기"))
.andDo(MockMvcResultHandlers.print());
}
}
...
public class PostControllerIntegreTest {
...
@Test
public void findById_fail_테스트() throws Exception {
// given
Long id = 1L;
// when
ResultActions resultAction = mockMvc.perform(get("/post/{id}", id)
.accept(MediaType.APPLICATION_JSON));
// then
resultAction
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("$.code").value("Item Not Found"))
.andExpect(jsonPath("$.message").value("id를 확인해주세요!!"))
.andDo(MockMvcResultHandlers.print());
}
}
...
public class PostControllerIntegreTest {
...
@Test
public void updateById_테스트() throws Exception {
// given
// data 생성
List<Post> postList = new ArrayList<>();
postList.add(new Post(null, "스프링부트 따라하기", "스프링부트 따라하기 내용"));
postList.add(new Post(null, "리액트 따라하기", "리액트 따라하기 내용"));
postRepository.saveAll(postList);
Long id = 1L;
WritePostReq writePostReq = WritePostReq.builder()
.title("스프링부트 또 따라하기")
.content("스프링부트 또 따라하기 내용")
.build();
String content = new ObjectMapper().writeValueAsString(writePostReq);
// when
ResultActions resultActions = mockMvc.perform(put("/post/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.content(content)
.accept(MediaType.APPLICATION_JSON));
// then
resultActions
.andExpect(status().isOk())
.andExpect(jsonPath("$.title").value("스프링부트 또 따라하기"))
.andDo(MockMvcResultHandlers.print());
}
}
...
public class PostControllerIntegreTest {
...
@Test
public void deleteById_테스트() throws Exception {
// given
// data 생성
List<Post> postList = new ArrayList<>();
postList.add(new Post(null, "스프링부트 따라하기", "스프링부트 따라하기 내용"));
postList.add(new Post(null, "리액트 따라하기", "리액트 따라하기 내용"));
postRepository.saveAll(postList);
Long id = 1L;
// when
ResultActions resultAction = mockMvc.perform(delete("/post/{id}", id));
// then
resultAction.andExpect(status().isOk()).andDo(MockMvcResultHandlers.print());
MvcResult requestResult = resultAction.andReturn();
String result = requestResult.getResponse().getContentAsString();
assertEquals("ok", result);
}
}