Todolist 단위 TestCode 만들기

devdo·2022년 2월 11일
0

TDD

목록 보기
3/6
post-thumbnail

테스트를 해야 하는 이유

Test는 class에 내용들을 작은 단위로 쪼개서 대역을 나누면서 한다고 보면 된다.

생각보다 익히기 어려운 부분이고 의식적으로 노력해야 한다고 생각한다.

이번엔 Todolist 프로젝트 그중 todolist_backend 소스를 가지고 TestCode를 만들어 보았다.


단위 테스트를 하면 좋은 점

그동안 버그를 확인하기 위해 sysout 또는 log로 위치를 확인하느라 굉장히 번거로웠다.
JUnit을 사용하여 단위테스트를 진행하니, 어디에서 오류가 았나는지 + 최적화 코드를 유추하기 훨씬 편하게 코드를 확인할 수 있다. 그리고

가장 중요한 점은 코드를 수정하는 데 테스트코드가 있으니 변화에 두려워지지 않는다!(CleanCode)

리팩토링 관점에서 수정에 두려움이 없어지는 데 Testcode는 이제 필수다.


Junit이란?

이 블로그를 참조하기를 바란다.
https://velog.io/@mooh2jj/Junit-기본-예제


단위테스트, 슬라이스 테스트?

슬라이스 테스트(slice test)란?
레이어를 독립적으로 테스트하기 위해 Mockito라이브러리 또는 @DataJpaTest를 활용했는데, 코드 리뷰를 받으면서 슬라이스 테스트라는 용어를 알게 되었다.

말 그대로 레이어별로 잘라서, 레이어를 하나의 단위로 보는 단위 테스트를 한다는 것이다.

그럼 Mockito라이브러리를 활용할 수 있는 Repository, Controller, Service가 슬라이스 테스트라 할 수 있겠다.


Junit5 테스트코드 설정

build.gradle

// 기본 장착
testImplementation 'org.springframework.boot:spring-boot-starter-test'

// h2 : test db용
runtimeOnly 'com.h2database:h2' 

// test에서 lombok 사용 -> @Sl4j 사용가능
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
// test sql 확인용
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.8.0'

application -> SpringBoot Profile 설정

☑️ 보통, profile test 구조에서 DB는 H2 를 사용한다.

✅ TestCode에 사용할려면 application 파일에 구성에 따라, 기본적으로 Class 파일에 @ActiveProfile("test") 또는 @Profile("test") 을 붙여야 한다.

1) Profile

spring:
  profiles: local

---
spring:
  profiles: dev
  
  
---
spring:
  profiles: prod
 
 ...

이런 구조의 설정이면, @Profile("test") 구성으로 해놓으면 된다.


2) ActiveProfile

예시)

application.yml (test폴더> resources폴더 or main폴더> resources폴더)

---
spring:
  config:
    activate:
      on-profile: test
  jpa:
    database: h2

이와 같은 application 프로퍼티 파일 구성이면, @ActiveProfile("test") 하면 된다.

💡 참고) test폴더> resources 폴더에 넣으면, application 프로퍼티 파일 없어도 testCode가 알아서 application "test" 파일을 잡는다.

@Slfj4
@ActiveProfiles("test")
@Import(JpaConfig.class)	// Config파일이 필요하다면 import

그리고

build.gradle runtimeOnly 'com.h2database:h2' 추가

여기서, H2 데이터베이스 를 추가 설치 x


test 디렉토리에서 application 파일에서 설정된 것이 없으면, 또는 아예 존재하지 않으면 스프링에서는 자동으로 임베디드 DB(h2:mem)를 설정해준다.

굳이 build.gradle에 runtimeOnly 'com.h2database:h2'도 할 필요도 없다.


✅참고)
application.yml을 main>resources에서도 설정가능하게 할려면?
(local, dev, prod 까지 설정이 가능)

그리고 ---로 분리할 수 있다.

spring:
  profiles: dev

---

spring:
  profiles: test
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:testdb;MODE=MySQL;
    username: sa
    password:

---

spring:
  profiles: prod
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:8080/db_name?serverTimezone=UTC&characterEncoding=UTF-8
    username: sa
    password:

✅ build 시, ./gradlew clean build 할시, testCode를 기본적으로 하게 되는데, @ActiveProfile("test") 또는 @Profile("test") 구성으로 할시 잘된다!

✅ 향후 jar 파일을 배포할 시 옵션 Arguments에 VM arguments로 구동시 변수 설정으로,
-Dspring.profiles.active=local 와 같이 추가할 수 있다.


Repository, Service, Controller 코드에 적용되는 TestCode가 다 다르다.

다음 각각 구현 방식을 살펴보자.


Layered 클래스별 TestCode 구현방식

보통, given → when → then에 맞춰 테스트 코드를 작성하는 게 가독성과 명확성 측면에서 좋다.

  • given: 테스트에 대한 pre-confition
  • when: 테스트하고 싶은 동작 호출
  • then: 테스트 결과 확인

then 메서드로는 assertJassertThat을 많이쓴다. 그리고 Mockitoverify를 사용할 수 있다.

Junit에서 의존성 주입은 @Autowired로 한다.
Junit에서는 DI를 지원하는 타입으로 @Autowired로 정해져 있다고 보면 된다.

생성자나 기타 다른 DI 방식 예를 들어,@RequiredArgConstructor 같은 어노테이션은 쓰지 못한다. TestCode에서 작성시 JUnit이 먼저 개입이 되다보니 에러가 나기 때문이다.

// 기본 TestCode 예제
public class HelloResponseDtoTest { 
    
    @Test 
    public void lombokFunctionTest() { 

        //given 
        String name = "test";
        int amount = 1000; 

        //when 
        HelloResponseDto dto = new HelloResponseDto(name, amount); 

        //then 
        assertThat(dto.getName()).isEqualTo(name); 
        assertThat(dto.getAmount()).isEqualTo(amount); 
    } 
}

테스트(DB) 어노테이션 정리

@SpringBootTest // 스프링 컨테이너와 테스트를 함께 실행한다, 통합테스트용
// @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) 통합테스트 환경을 Mock화

// @Transactional  //테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고,
                // 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지않는다.
// @DataJpaTest  // 오직 JPA 컴포넌트들만 로드함, insert쿼리 안날림, @Transactional(자동Rollback)가 기본적으로 설정됨. 설정해놓은 DB가 아닌 in-memory DB를 활용해서 테스트가 실행됨.(h2 디팬더시 필요)

// @Commit or @Rollback(value = false)  // 롤백을 막아줌, 스터디시 테이블에 데이터가 들어가는 것을 확인하기 위해 설정

@ActiveProfiles("test")
// application-test.yml이 동작될때, 실행됨

@Profile("test")
// 무조건 application-test.yml이 동작된다!
// 만약, Test 코드 외에 다른 코드에 이 어노테이션을 사용하면 무조건 application-test.yml이 실행된다는 말이다!
// 1.@BeforeAll 2.@BeforeEach 3.@AfterEach 4.@AfterAll
@BeforeAll // 이 가장 먼저 실행 
// ----- 반복
@BeforeEach // 가 실행
@Test // 붙인 메서드 실행
@AfterEach // 실행
//  ------ 반복
@AfterAll // 가장 마지막 실행

통합테스트 예시는 이블로그에서 참조해주시면 된다.

https://velog.io/@mooh2jj/Todolist-통합TestCode-만들기

public
public 메서드만 스프링 트랜잭션 AOP @Transactional 기능이 적용된다! // test 코드 작성할 때 주의!
참고로 public 이 아닌곳에 @Transactional이 붙어 있으면 예외가 발생하지 않고, 트랜잭션 적용만 무시된다.


1. Repository

  • JPA메서드를 테스트하는 경우가 많고 DB설정을 어떻게 하느냐에 따라, 실제 상용하는 DB에 등록할 수 있으니 설정에 주의해야 한다.
  • application.yml 설정에 DB설정을 하지 않으면 자동으로 H2(in-memory) DB에 등록된다.
    * 그러나 H2 디팬더시는 설정해줘야 한다.

build.gradle

// RepositoryTest h2데이터에서 설정위함
runtimeOnly 'com.h2database:h2'

@Slf4j
@DataJpaTest
class TodoRepositoryTest {

    @Autowired
    TodoRepository todoRepository;

    TodoEntity todoEntity;

    @BeforeEach
    public void setup() {
        todoEntity = TodoEntity.builder()
                .title("testTodo")
                .order(0L)
                .completed(false)
                .build();
    }

/*    @AfterEach
    void afterEach() {
        todoRepository.deleteAll();		// rollback을 해주기에 필요x
    }*/

    @DisplayName("save 테스트")
    @Test
    public void save(){
        // given - precondition or setup
        // when - action or the behaviour that we are going test
        TodoEntity savedTodo = todoRepository.save(todoEntity);

        // then - verify the output
        assertThat(savedTodo).isNotNull();
        assertThat(savedTodo.getId()).isGreaterThan(0);
        assertThat(savedTodo.getTitle()).isEqualTo("testTodo");

    }


    @DisplayName("add 20개 등록 테스트")
    @Test
    public void addTest() {

        IntStream.rangeClosed(1,20).forEach(i -> {
            TodoEntity todoEntity = TodoEntity.builder()
                    .title("todo_dsg" + i)
                    .order((long) i)
                    .completed(true)
                    .build();
            todoRepository.save(todoEntity);
        });
    }

    @DisplayName("getAll 테스트")
    @Test
    public void getAll() {

        TodoEntity todoEntity1 = TodoEntity.builder()
                .title("testTodo1")
                .order(1L)
                .completed(false)
                .build();

        todoRepository.save(todoEntity);
        todoRepository.save(todoEntity1);

        List<TodoEntity> all = todoRepository.findAll();
        log.info("todoEntity(all): {}", all);

        assertThat(all).isNotNull();
        assertThat(all.size()).isEqualTo(2);      // DB H2일시 true, 실제 DB이면 오류 날 수 있어!
    }

    @DisplayName("getById 테스트")
    @Test
    public void getById() {
    
        todoRepository.save(todoEntity);
        
        TodoEntity todoEntity = todoRepository.getById(1L);
        log.info("todoEntity(getOne): {}", todoEntity);

        assertThat(todoEntity).isNotNull();
    }

    @DisplayName("update 테스트")
    @Test
    public void update() {
        TodoEntity todoEntity = TodoEntity.builder()
                .id(19L)
                .title("updatedTitle")
                .order(19L)
                .completed(true)
                .build();
        log.info("todoEntity: {}", todoRepository.save(todoEntity));

        assertThat(todoEntity.getTitle()).isEqualTo("updatedTitle");
    }

    @DisplayName("delete 테스트")
    @Test
    public void deleteById(){
        // given - precondition or setup
        TodoEntity savedTodoEntity = todoRepository.save(this.todoEntity);
        // when - action or the behaviour that we are going test
        // todoRepository.deleteById(savedTodoEntity.getId());	// 오류가 난다면 아래로!
		todoRepository.delete(savedTodoEntity);        
        Optional<TodoEntity> deletedTodo = todoRepository.findById(savedTodoEntity.getId());

        // then - verify the output
        assertThat(deletedTodo).isEmpty();

    }

    // Page

    @Test
    public void testPage() {
        // Pageable pageable = PageRequest.of(0, 10);
		Pageable pageable = PageRequest.of(0, 10, Sort.by("id").descending());

        Page<TodoEntity> result = todoRepository.findAll(pageable);
        log.info("result: {}", result);
    }

    @DisplayName("정렬하기 테스트")
    @Test
    public void sortTest() {
//        var todoEntities = todoRepository.findAll(Sort.by(Sort.Direction.DESC, "order"));
//        todoEntities.forEach(System.out::println);
        Sort sort = Sort.by("id").descending();
        Pageable pageable = PageRequest.of(0, 10, sort);

        Page<TodoEntity> result = todoRepository.findAll(pageable);

        result.get().forEach(System.out::println);
    }

    @DisplayName("1개 id로 1row 찾기 테스트")
    @Test
    public void byIdTest() {
        var todoEntity = todoRepository.findById(10L);
        log.info("todoEntity: {}", todoEntity.orElse(null));
    }

    @DisplayName("Arrays.asList로 list 찾기 테스트")
    @Test
    public void listTest() {

        List<TodoEntity> allById = todoRepository.findAllById(Arrays.asList(1L, 2L, 3L));
        allById.forEach(System.out::println);

    }

}

2. Service

service부터는 Mockito 라이브러리를 사용해 Mocking 테스트가 이루어진다. Mock이란 가짜란 뜻이다. 주로 DI 개념의 인터페이스를 가져올 때 가짜 객체로 주입할 때 사용한다.

Mock 테스트 순서
1) CreateMock : 인터페이스에 해당하는 Mock 객체를 만든다.
2) Stub : 테스트에 필요한 Mock 객체의 동작을 지정한다. (필요시만)
3) Exercise : 테스트 메스드 내에서 Mock 객체를 사용한다.
4) Verify : 메서드가 예상대로 호출됐는지 검증한다.

그냥 given - when - then 으로 생각하면 된다!

// serviceTest 주요 코드 예시
@ExtendWith(MockitoExtension.class)

// mock 객체를 생성한다.
@Mock
private TodoRepository todoRepository;

// @Mock이 붙은 목객체를 
// @InjectMocks이 붙은 객체에 주입시킬 수 있다
@InjectMocks
private TodoService todoService;

// given-when-then 구조
@Test
void test(){
	// given
    given(todoRepository.findAll())
             .willReturn(List.of(todo1, todo2));
    // when
    List<TodoEntity> todos = todoService.searchAll();
    
    // then
    assertThat(todos.size()).isEqualTo(2);
                
}

** given(memberService.list()).willReturn(members);

가짜객체가 원하는 행위를 할 수 있도록 정의해준다.(given, when 등을 사용) => BDDMockito의 given()을 많이 사용함

💥아래 error 문구가 나온다면 when() 구문을 사용해 볼것!

  • stubbing the same method multiple times using 'given().will()' or 'when().then()' API
    Please use 'will().given()' or 'doReturn().when()' API for stubbing.
  • stubbed method is intentionally invoked with different arguments by code under test
    Please use default or 'silent' JUnit Rule (equivalent of Strictness.LENIENT).

memberService의 list() 메서드를 실행시키면 members를 리턴해달라는 요청이다.

** lenient

Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.

lenient :
불필요한 스터빙을 하지 않도록 되어있는데, 현재 코드에 쓰이지 않는 스터빙을 해놨기 때문에 저런 메시지가 보이는 것이고, lenient는 그런 제약을 느슨하게 허용하는 것이다.

lenient().given(memberService.list()).willReturn(members);

2) assertThat(savedMember).isNotNull();, verify(memberService).insert(member);
해당 메서드가 실행됐는지를 검증해준다.


when(), verify(), assertThat()을 쓰기 위해, 다음 라이브러리 import가 꼭 필요!

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

구현 예시

// Mockito : Mock을 지원하는 자바 테스트 프레임워크
@Slf4j
@ExtendWith(MockitoExtension.class)
public class TodoServiceTest {

    @InjectMocks
    private TodoServiceImpl todoService;
    // Impl로! mockito @InjectMock 는 구현체를 주입!

    @Mock
    private TodoRepository todoRepository;

    private TodoEntity todoEntity;

    @BeforeEach
    public void setup() {
        todoEntity = TodoEntity.builder()
                .id(1L)
                .title("testTodo")
                .order(0L)
                .completed(false)
                .build();
    }

    @Test
    public void add() {
        when(this.todoRepository.save(any(TodoEntity.class)))
                .then(AdditionalAnswers.returnsFirstArg());
//        given(todoRepository.save(todoEntity)).willReturn(todoEntity);

        TodoRequest todoRequest = TodoRequest.builder()
                .title(todoEntity.getTitle())
                .order(todoEntity.getOrder())
                .completed(todoEntity.getCompleted())
                .build();
        TodoResponse savedResponse = this.todoService.add(todoRequest);

        log.info("savedResponse: {}", savedResponse);
//        assertEquals(1L,  savedResponse.getId());       //  test 오류 null 뜨는지 이해 불가.
//        assertEquals("testTodo",  savedResponse.getTitle());
        assertThat(savedResponse).isNotNull();
        assertThat(savedResponse.getTitle()).isEqualTo(todoRequest.getTitle());
    }

    @Test
    public void searchById() {

        Optional<TodoEntity> expected = Optional.of(todoEntity);

        given(this.todoRepository.findById(1L))
                .willReturn(expected);

        TodoResponse response = this.todoService.searchById(1L);

        log.info("response: {}", response);
        
//        assertEquals(response.getOrder(), 0L);
//        assertFalse(response.getCompleted());
//        assertEquals(response.getTitle(), "testTodo");
        assertThat(response.getTitle()).isEqualTo("testTodo");
        assertThat(response.getOrder()).isEqualTo(0L);
    }


    // 에러 발생 테스트도 만듦
    @Test
    public void searchById_ThrowsException() {
        given(this.todoRepository.findById(anyLong())).willReturn(Optional.empty());

//        assertThrows(ResponseStatusException.class, () -> {
//            this.todoService.searchById(1L);
//        });
        assertThatThrownBy(() -> {
            todoService.searchById(1L);
        }).isInstanceOf(ResponseStatusException.class);
    }
    
    // 에러 메시지 확인 테스트
    @Test
    @DisplayName("Member 중복 테스트")
    public void saveDuplicateMemberTest() {

        memberRepository.save(member);

        assertThatThrownBy(() -> {
            memberService.saveMember(member);
        }).isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("이미 가입된 회원입니다.");
        
    }

    @Test
    public void searchAll(){
        // given - precondition or setup
        TodoEntity todoEntity1 = TodoEntity.builder()
                .id(100L)
                .title("test_dsg")
                .order(1L)
                .completed(true)
                .build();
        given(todoRepository.findAll()).willReturn(List.of(todoEntity, todoEntity1));

        // when - action or the behaviour that we are going test
        List<TodoResponse> todoResponses = todoService.searchAll();
        log.info("todoResponses: {}", todoResponses);

        // then - verify the output
        assertThat(todoResponses).isNotNull();
        assertThat(todoResponses.size()).isEqualTo(2);

    }

    @Test
    public void searchAll_negative(){
        // given - precondition or setup
        given(todoRepository.findAll()).willReturn(Collections.emptyList());
        // when - action or the behaviour that we are going test
        List<TodoResponse> todoResponses = todoService.searchAll();
        log.info("todoResponses: {}", todoResponses);
        // then - verify the output
        assertThat(todoResponses).isEmpty();
        assertThat(todoResponses.size()).isEqualTo(0);

    }

    @Test
    public void updateById(){
        // given - precondition or setup
        given(todoRepository.findById(anyLong()))
                .willReturn(Optional.of(todoEntity));
        given(todoRepository.save(todoEntity)).willReturn(todoEntity);      // updateById면 두 상황 모두 만들어져야돼!

        TodoRequest request = TodoRequest.builder()
                .title("kkk")
                .order(2L)
                .build();

        // when - action or the behaviour that we are going test
        TodoResponse todoResponse = todoService.updateById(todoEntity.getId(), request);
        log.info("todoResponses: {}", todoResponse);
        // then - verify the output
        assertThat(todoResponse.getTitle()).isEqualTo(request.getTitle());
        assertThat(todoResponse.getOrder()).isEqualTo(request.getOrder());

    }

    @Test
    public void deleteById(){
        // given - precondition or setup
        Long todoId = 1L;
        willDoNothing().given(todoRepository).deleteById(todoId);
		// 안될시
        // given(todoRepository.findById(anyLong())).willReturn(Optional.of(todoEntity));
        // when - action or the behaviour that we are going test
        todoService.deleteById(todoId);
        // then - verify the output
        verify(todoRepository).deleteById(todoId);

    }
}

3. Controller

주로 MockMvc mvc객체로 검증한다.

// Controller 레이어 기본 세팅
// 특정레이어만 test (vs @SpringBootTest: 전체) 
//  @Controller @RestController @ControllerAdvice 등등 컨트롤러와 연관된 bean들이 로드됨
@WebMvcTest(TodoController.class)

// MockMvc: api 테스트용으로 시뮬레이션하여 MVC가 되도록 도와주는 클래스
@Autowired
private MockMvc mvc;
    
// @MockBean: ApplicationContext에 mock객체를 추가
@MockBean
private TodoService todoService;
  • contentType(MediaType.APPLICATION_JSON)
  • ObjectMapper 선언, String content = mapper.writeValueAsString(request); : 객체 -> String으로 직렬화
  • 참고) ObjectMapper readValue()
// 참고로 mapper.readValue()는 역직렬화(String -> Object)
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// do various things, perhaps:
String someJsonString = mapper.writeValueAsString(someClassInstance);
SomeClass someClassInstance = mapper.readValue(someJsonString, SomeClass.class)
  • mvc.perform(get("/hello")) : MockMvc를 통해 /hello 주소로 HTTP GET 요청

  • .andExpect(status().isOk()) : mvc.perform의 결과를 검증

  • .param : API테스트할 때 사용될 요청 파라미터를 설정한다.

  • .andExpected(jsonPath("$.name", is(name))) : JSON 응답값을 필드별로 검증할 수 있는 메소드, $를 기준으로 필드명을 명시한다.

  • .andDo(print()) : 요청,응답에 대해서 콘솔창에 print해줌!

관련된 메서드들은 다음 MockMvcRequestBuilders, MockMvcResultMatchers 객체가 필요하다, 기억해두면 좋다!


import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willDoNothing;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;


@WebMvcTest(TodoController.class)
public class TodoControllerTest {

    @Autowired
    MockMvc mvc;

    @MockBean
    private TodoServiceImpl todoService;

    @Autowired
    ObjectMapper mapper;

    private TodoEntity expected;

    @BeforeEach
    void setup() {
        this.expected = new TodoEntity();
        this.expected.setId(123L);
        this.expected.setTitle("test");
        this.expected.setOrder(0L);
        this.expected.setCompleted(false);
    }


    @Test
    void create() throws Exception {
//        when(this.todoService.add(any(TodoRequest.class)))
//                .then((i) -> {
//                    TodoRequest request = i.getArgument(0, TodoRequest.class);
//                    return new TodoResponse(this.expected.getId(), request.getTitle(), request.getOrder(), request.getCompleted());
//                });

        TodoRequest todoRequest = TodoRequest.builder()
                .title(expected.getTitle())
                .order(expected.getOrder())
                .completed(expected.getCompleted())
                .build();

        TodoResponse todoResponse = TodoResponse.builder()
                .id(expected.getId())
                .title(todoRequest.getTitle())
                .order(todoRequest.getOrder())
                .completed(todoRequest.getCompleted())
                .build();

        given(todoService.add(todoRequest)).willReturn(todoResponse);

        String content = mapper.writeValueAsString(request);
// MockMvcRequestBuilders 객체 사용 
        mvc.perform(post("/todo")
                .contentType(MediaType.APPLICATION_JSON)
                .content(content))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(expected.getId()))
                .andExpect(jsonPath("$.title").value(expected.getTitle()))
                .andExpect(jsonPath("$.order").value(expected.getOrder()))
                .andExpect(jsonPath("$.completed").value(expected.getCompleted()))
                .andDo(print());
                
         verify(todoService).add(todoRequest);
    }

    @Test
    void readOne() throws Exception {

        TodoResponse todoResponse = TodoResponse.builder()
                .id(expected.getId())
                .title(expected.getTitle())
                .order(expected.getOrder())
                .completed(expected.getCompleted())
                .build();

        given(todoService.searchById(123L)).willReturn(todoResponse);

        mvc.perform(get("/todo/{id}", 123L))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.id").value(expected.getId()))
                .andExpect(jsonPath("$.title").value(expected.getTitle()))
                .andExpect(jsonPath("$.order").value(expected.getOrder()))
                .andExpect(jsonPath("$.completed").value(expected.getCompleted()));
    }

    @Test
    void readOneException() throws Exception {
        given(todoService.searchById(123L)).willThrow(new ResponseStatusException(HttpStatus.NOT_FOUND));

        mvc.perform(get("/todo/{id}", 123L))
                .andExpect(status().isNotFound());
    }

    @Test
    void readAll() throws Exception {
        int expectedLength = 10;
        List<TodoResponse> mockList = new ArrayList<>();
//        for (int i = 0; i < expectedLength; i++) {
//            mockList.add(todoResponse);
//        }
        IntStream.rangeClosed(1,expectedLength).forEach(i -> {
            TodoResponse todoResponse = TodoResponse.builder()
                    .title(expected.getTitle()+" "+i)
                    .order(expected.getOrder())
                    .completed(expected.getCompleted())
                    .build();
            mockList.add(todoResponse);
        });

        given(todoService.searchAll()).willReturn(mockList);

        mvc.perform(get("/todo"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()").value(expectedLength));
    }

    @Test
    public void updateById() throws Exception {

        Long todoId = 123L;
        TodoRequest todoRequest = TodoRequest.builder()
                .title("new_title")
                .order(1L)
                .completed(true)
                .build();

        TodoResponse todoResponse = TodoResponse.builder()
                .id(todoId)
                .title(todoRequest.getTitle())
                .order(todoRequest.getOrder())
                .completed(todoRequest.getCompleted())
                .build();

//        given(todoService.updateById(todoId))     // 오류남
//                .willAnswer((v) -> v.getArgument(0));
        given(todoService.updateById(todoId)).willReturn(todoResponse);


        mvc.perform(put("/todo/{id}", todoId)
                        .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.title", CoreMatchers.is(todoResponse.getTitle())))
                .andExpect(jsonPath("$.order", CoreMatchers.is(1)))     // 1L이라 오류날수 있어 value 1 넣음
                .andExpect(jsonPath("$.completed", CoreMatchers.is(todoResponse.getCompleted())));


    }

    @Test
    public void deleteById() throws Exception {
        Long todoId = 123L;
        willDoNothing().given(todoService).deleteById(todoId);

        mvc.perform(delete("/todo/{id}", todoId))
                .andDo(print())
                .andExpect(status().isOk());

    }

    @Test
    void deleteAll() throws Exception {
        mvc.perform(delete("/todo"))
                .andExpect(status().isOk());
    }
}

✅ 4. Entity 중심의 단위 테스트

// @SpringBootTest 필요 x, application-test.yml 필요 x
// @Transactional 필요 x
@Slf4j
class CouponRecordTest extends DummyObject {


    @Test
    @DisplayName("사용하지 않은 쿠폰, 사용 상태로 변경")
    void changeCouponStatusToUsed() {
        // Given
        CouponRecord couponRecord = createCouponRecord();
        log.info("couponRecord: {}", couponRecord);

        // When
        couponRecord.changeCouponStatusToUsed();

        // Then
        assertThat(couponRecord.getStatus()).isEqualTo(CouponRecordStatus.USED);
        assertThat(couponRecord.getUsedDatetime()).isNotNull();
    }


    @Test
    @DisplayName("만료된 쿠폰, 만료 상태로 변경")
    void changeExpiredCouponStatus() {
        // Given
        CouponRecord couponRecord = createCouponRecord();
        log.info("couponRecord: {}", couponRecord);

        // When
        couponRecord.changeExpiredCouponStatus();

        // Then
        assertThat(couponRecord.getStatus()).isEqualTo(CouponRecordStatus.EXPIRED);
    }
}

💥오류

contextLoads() FALIED error
운영에서 테스트 중 위와 같은 에러로 빌드가 계속 실패하였는데
프로젝트명 ApplicationTests.java 파일의 @SpringBootTest 어노테이션을 주석처리하거나 test 폴더 하위에 *.yml 파일을 두시면 해결 될 것입니다.



참고

profile
배운 것을 기록합니다.

0개의 댓글