전체 코드는 여기에서 볼 수 있습니다.

 

1. 유저 생성 API - Controller, Service, Repository, Entitiy, UserDTO

- controller

@RestController
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    @PostMapping("/user")
    @ResponseStatus(value = HttpStatus.CREATED)
    public SuccessResponse<UserDTO> createUser(@RequestBody @Valid UserDTO userDTO) {
        User user = userService.createUser(userDTO);

        return SuccessResponse.success(UserDTO.builder()
                .id(user.getId())
                .build());
    }
}

 

- service

@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;

    public User createUser(UserDTO userDTO) {
        User user = User.builder()
                .email(userDTO.getEmail())
                .password(userDTO.getPassword())
                .nickname(userDTO.getNickname())
                .createdBy(LocalDateTime.now())
                .build();

        return userRepository.save(user);
    }
}

 

- repository

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

 

- entity

@Entity
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String email;

    private String password;

    private String nickname;

    private LocalDateTime createdBy;
}

 

- userDTO

@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UserDTO {
    private Long id;

    @Email(message = "이메일 양식이 아닙니다.")
    @NotBlank(message = "이메일은 필수값 입니다.")
    private String email;

    @NotBlank(message = "비밀번호는 필수값 입니다.")
    private String password;

    @NotBlank(message = "닉네임은 필수값 입니다.")
    private String nickname;

    private LocalDateTime createdBy;
}

 

2. 유저 생성 테스트코드 작성 - ControllerTest, ServiceTest

- controllerTest

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class UserControllerTest {
    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private UserService userService;

    public UserDTO initUserDTO() {
        return UserDTO.builder()
                .email("email@email.com")
                .password("password")
                .nickname("nickname")
                .build();
    }

    @Test
    public void 유저_생성() throws Exception {
        // given
        UserDTO userDTO = initUserDTO();

        // when
        ResultActions perform = mockMvc.perform(post("/user")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(userDTO)));

        // then
        perform
                .andExpect(status().isCreated())
                .andExpect(jsonPath("message").exists())
                .andExpect(jsonPath("message").value("success"))
                .andExpect(jsonPath("data").hasJsonPath())
                .andExpect(jsonPath("data.id").exists())
                .andExpect(jsonPath("data.id").value(1L))
                .andDo(print());
    }

    @Test
    public void 유저_생성_이메일_양식아님() throws Exception {
        // given
        UserDTO userDTO = initUserDTO();
        userDTO.setEmail("email");

        // when
        ResultActions perform = mockMvc.perform(post("/user")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(userDTO)));

        // then
        perform
                .andExpect(jsonPath("message").exists())
                .andExpect(jsonPath("message").value("bad_request"))
                .andExpect(jsonPath("errors").hasJsonPath())
                .andExpect(jsonPath("errors", hasSize(1)))
                .andExpect(jsonPath("errors[*].field", containsInAnyOrder("email")))
                .andExpect(jsonPath("errors[*].message", containsInAnyOrder("이메일 양식이 아닙니다.")))
                .andExpect(status().isBadRequest())
                .andDo(print());
    }

    @Test
    public void 유저_생성_이메일_없음() throws Exception {
        // given
        UserDTO userDTO = initUserDTO();
        userDTO.setEmail("");

        // when
        ResultActions perform = mockMvc.perform(post("/user")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(userDTO)));

        // then
        perform
                .andExpect(jsonPath("message").exists())
                .andExpect(jsonPath("message").value("bad_request"))
                .andExpect(jsonPath("errors").hasJsonPath())
                .andExpect(jsonPath("errors", hasSize(1)))
                .andExpect(jsonPath("errors[*].field", containsInAnyOrder("email")))
                .andExpect(jsonPath("errors[*].message", containsInAnyOrder("이메일은 필수값 입니다.")))
                .andExpect(status().isBadRequest())
                .andDo(print());
    }

    @Test
    public void 유저_생성_패스워드_없음() throws Exception {
        // given
        UserDTO userDTO = initUserDTO();
        userDTO.setPassword("");

        // when
        ResultActions perform = mockMvc.perform(post("/user")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(userDTO)));

        // then
        perform
                .andExpect(jsonPath("message").exists())
                .andExpect(jsonPath("message").value("bad_request"))
                .andExpect(jsonPath("errors").hasJsonPath())
                .andExpect(jsonPath("errors", hasSize(1)))
                .andExpect(jsonPath("errors[*].field", containsInAnyOrder("password")))
                .andExpect(jsonPath("errors[*].message", containsInAnyOrder("비밀번호는 필수값 입니다.")))
                .andExpect(status().isBadRequest())
                .andDo(print());
    }

    @Test
    public void 유저_생성_닉네임_없음() throws Exception {
        // given
        UserDTO userDTO = initUserDTO();
        userDTO.setNickname("");

        // when
        ResultActions perform = mockMvc.perform(post("/user")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(userDTO)));

        // then
        perform
                .andExpect(jsonPath("message").exists())
                .andExpect(jsonPath("message").value("bad_request"))
                .andExpect(jsonPath("errors").hasJsonPath())
                .andExpect(jsonPath("errors", hasSize(1)))
                .andExpect(jsonPath("errors[*].field", containsInAnyOrder("nickname")))
                .andExpect(jsonPath("errors[*].message", containsInAnyOrder("닉네임은 필수값 입니다.")))
                .andExpect(status().isBadRequest())
                .andDo(print());
    }
}

 

- serviceTest

기존에는 Mock을 활용하여 유닛테스트를 하였으나 과연 실제 상황에서 잘 작동하는가에 대해 의문이 들어 통합테스트로 변경하였습니다.

@SpringBootTest
@EnableAutoConfiguration
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class PostServiceTotalTest {
    @Autowired
    private PostService postService;

    @Autowired
    private PostRepository postRepository;

    public PostDTO initPostDTO() {
        return PostDTO.builder()
                .title("title")
                .contents("contents")
                .build();
    }

    public Post initPost(PostDTO postDTO) {
        return Post.builder()
                .title(postDTO.getTitle())
                .contents(postDTO.getContents())
                .createAt(LocalDateTime.now())
                .build();
    }

    @Test
    @DisplayName("게시물 등록하기 테스트")
    @Order(1)
    public void createPost() throws Exception {
        //given
        PostDTO postDTO = initPostDTO();

        // when
        PostDTO post1 = postService.createPost(postDTO);

        // then
        assertThat(post1.getId()).isEqualTo(1L);
    }

    @Test
    @Order(2)
    public void 게시물_조회() throws Exception {
        // when
        PostDTO post = postService.getPost(1L);

        // then
        assertThat(post.getId()).isEqualTo(1L);
        assertThat(post.getTitle()).isEqualTo("title");
        assertThat(post.getContents()).isEqualTo("contents");
    }

    @Test
    @Order(3)
    @Transactional
    public void 게시물_제목_업데이트() throws Exception {
        Post findPost = postRepository.findById(1L).get();

        findPost.changeTitle("title update");

        Post updatePost = postRepository.save(findPost);

        assertThat(updatePost.getId()).isEqualTo(findPost.getId());
        assertThat(updatePost.getTitle()).isEqualTo("title update");
        assertThat(updatePost.getContents()).isEqualTo("contents");
    }

    @Test
    @Order(4)
    @Transactional
    public void 게시물_내용_업데이트() throws Exception {
        Post findPost = postRepository.findById(1L).get();

        findPost.changeContents("contents update");

        Post updatePost = postRepository.save(findPost);

        assertThat(updatePost.getId()).isEqualTo(findPost.getId());
        assertThat(updatePost.getTitle()).isEqualTo("title");
        assertThat(updatePost.getContents()).isEqualTo("contents update");
    }

    @Test
    @Transactional
    public void 게시물_삭제_성공() throws Exception {
        Post post = postRepository.findById(1L).get();

        postRepository.delete(post);

        assertThatThrownBy(() -> {
            postService.getPost(1L);
        }).isInstanceOf(PostNotFound.class)
                .hasMessage("해당 포스트가 존재하지 않습니다.");
    }
}

왜 테스트 코드를 작성해야 하나요?

테스트 코드를 작성을 왜 하는지 궁금하신 분들이 계실겁니다.

물론 테스트 코드를 작성하지 않고 API를 만든 후에 포스트맨같은 툴로 API를 테스트 할 수도 있을겁니다. 저 역시 이전에는 그렇게 하였구요. 그러나 이 방법의 최대 단점은 손이 많이 간다는 것입니다.

예를 들어 회원가입을 한다면 확인해야 할 것이 아래와 같습니다.

  1. Email이 중복이 아닌가?
  2. 비밀번호가 양식에 맞는가?
  3. 필수로 입력해야할 칸이 비어있지는 않는가?
  4. 가입이 성공적으로 완료되었을때 응답을 올바르게 오는가?
  5. 1,2,3번에 해당하였을 경우 에러를 잘 뱉어내는가?

위와 같은 테스트 케이스들이 많습니다! 무려 회원가입 API 하나만 체크하는데도 최소 5개가 되는 케이스를 테스트 해야합니다.

이걸 포스트맨으로 일일히 옮겨 적는다면.. 예전 생각이 나서 정말 끔찍하네요.

이러한 노가다를 방지하기 위해 테스트 코드를 작성하는데요? 물론 테스트코드 역시 위와 같은 케이스를 모두 작성해주어야 하는 번거로움은 있습니다. 그러나 테스트코드를 작성하면 위와같은 케이스를 한번 클릭으로 모두 테스트할 수 있기때문에 시간 절약이 됩니다!

 

Controller Test

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class PostControllerTest {
    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private PostService postService;

    public PostDTO initPostDTO() {
        return PostDTO.builder()
                .title("title")
                .contents("contents")
                .build();
    }

    @Test
    @DisplayName("게시물 등록 테스트")
    @Order(1)
    public void createPost() throws Exception {
        // given
        PostDTO postDTO = initPostDTO();

        //when
        ResultActions resultActions = mockMvc.perform(post("/post")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(postDTO)));

        //then
        resultActions
                .andExpect(jsonPath("data").exists())
                .andExpect(jsonPath("data").hasJsonPath())
                .andExpect(jsonPath("data.id").exists())
                .andExpect(jsonPath("message").value("success"))
                .andExpect(jsonPath("data.id").value(1))
                .andExpect(status().isCreated())
                .andDo(print());
    }

    @Test
    @DisplayName("게시물 가져오기 테스트 - 성공")
    @Order(2)
    public void getPostSuccess() throws Exception {
        // given
        PostDTO postDTO = initPostDTO();
        postService.createPost(postDTO);

        // when
        ResultActions perform = mockMvc.perform(get("/post/{id}", 1));

        // then
        perform
                .andExpect(status().isOk())
                .andExpect(jsonPath("message").exists())
                .andExpect(jsonPath("message").value("success"))
                .andExpect(jsonPath("data").exists())
                .andExpect(jsonPath("data.id").exists())
                .andExpect(jsonPath("data.title").exists())
                .andExpect(jsonPath("data.title").value("title"))
                .andExpect(jsonPath("data.contents").exists())
                .andExpect(jsonPath("data.contents").value("contents"))
                .andDo(print());
    }

    @Test
    @DisplayName("존재하지않는 게시물 가져오기")
    @Order(3)
    public void getPostFail() throws Exception {
        mockMvc.perform(get("/post/{id}", 9999999))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("message").exists())
                .andExpect(jsonPath("message").value("error"))
                .andExpect(jsonPath("errors[0]").exists())
                .andExpect(jsonPath("errors[0].field").doesNotExist())
                .andExpect(jsonPath("errors[0].message").exists())
                .andExpect(jsonPath("errors[0].message").value("해당 포스트가 존재하지 않습니다."))
                .andDo(print());
    }

    @Test
    public void 게시물_제목_입력안함() throws Exception {
        // given
        PostDTO postDTO = PostDTO.builder()
                .contents("contents")
                .build();

        //when
        ResultActions resultActions = mockMvc.perform(post("/post")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(postDTO)));

        //then
        resultActions
                .andExpect(jsonPath("errors").exists())
                .andExpect(jsonPath("errors").isArray())
                .andExpect(jsonPath("errors[*].field", containsInAnyOrder("title")))
                .andExpect(jsonPath("errors[*].message", containsInAnyOrder("제목을 입력해주세요.")))
                .andExpect(status().isBadRequest())
                .andDo(print());
    }

    @Test
    public void 게시물_내용_입력안함() throws Exception {
        // given
        PostDTO postDTO = PostDTO.builder()
                .title("title")
                .build();

        //when
        ResultActions resultActions = mockMvc.perform(post("/post")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(postDTO)));

        //then
        resultActions
                .andExpect(jsonPath("errors").exists())
                .andExpect(jsonPath("errors").isArray())
                .andExpect(jsonPath("errors[*].field", containsInAnyOrder("contents")))
                .andExpect(jsonPath("errors[*].message", containsInAnyOrder("내용을 입력해주세요.")))
                .andExpect(status().isBadRequest())
                .andDo(print());
    }

    @Test
    public void 게시물_내용_제목_입력안함() throws Exception {
        // given
        PostDTO postDTO = PostDTO.builder()
                .build();

        //when
        ResultActions resultActions = mockMvc.perform(post("/post")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(postDTO)));

        //then
        resultActions
                .andExpect(jsonPath("errors").exists())
                .andExpect(jsonPath("errors").isArray())
                .andExpect(jsonPath("errors").hasJsonPath())
                .andExpect(jsonPath("errors", hasSize(2)))
                .andExpect(jsonPath("errors[*].field", containsInAnyOrder("title", "contents")))
                .andExpect(jsonPath("errors[*].message", containsInAnyOrder("제목을 입력해주세요.", "내용을 입력해주세요.")))
                .andExpect(status().isBadRequest())
                .andDo(print());
    }

}

 

 

errors[*] 문법은 errors의 원소의 순서없이 containsInAnyOrder의 값이 있냐없냐를 체크하기 위함입니다.

진행 코드는 여기서 볼 수 있습니다.

+ Recent posts