테스트 코드 (TDD) -매우매우 초보자용
전문적인 지식보다는 개인적인 경험을 다룬 게시물입니다..!
같이 성장을 원하시는 초보자 분들께 추천드립니다.
우리가 프로잭트는 딱! 만들면 보통 우리는 코드치기 바쁘다. 패키지 구성하고.. Entity Table 구성하고 열심히 개발해 나간다.
하지만 우리가 간과하는 것이 있다. 언제나 TDD에 대한 연습은 충분해야 한다는 것이며
개인적으로 생각해 보았을 때 TDD를 작성하게 된다면 지금 내가 하고있는 이 프로잭트에 대한 이해도가 높아진다.
클라이언트쪽에서 " ~~ 문제는요?" 라고 물어보았을 때 대답을 하지 못하면 그것 만큼 내 자신이 창피해지는 상황이 없는것 같다... ㅠㅠ
그래서 태스트가 어떻게 진행이 되는거냐구?
세팅

IntelliJ 에서 프로잭트를 만들면 중간위치에 test 가 있을 것이다 이 안에 작성을 시작한다.
우선 내 프로잭트의 본 코드를 보고 시작하겠다.
Entity
@Builder @AllArgsConstructor @NoArgsConstructor
@Getter @Setter
@EqualsAndHashCode(of = "id")
@Entity
public class Lecture {
@Id @GeneratedValue
private Integer id;
private String name;
private String description;
private LocalDateTime beginDateTime;
private LocalDateTime endDateTime;
private String location; //(optional) 없으면 온라인 모임인거야. 위치가없으니까.
private int price; //optional
private boolean offline;
private boolean free;
public void updateBoolean() {
if(this.price == 0 ){
this.free = true;
}else{
this.free = false;
}
if(this.location == null){
this.offline = false;
}else{
this.offline = true;
}
}
}
Dto
@Builder @NoArgsConstructor @AllArgsConstructor @Data
public class LectureDto {
@NotEmpty
private String name;
@NotEmpty
private String description;
@NotNull
private LocalDateTime beginDateTime;
@NotNull
private LocalDateTime endDateTime;
private String location; //(optional) 없으면 온라인 모임인거야. 위치가없으니까.
@Min(0)
private int price; //optional
}
Errors(Validator) - 종강시간이 개강 시간뽀다 빠를경우 error 나게 했음 .
@Component
public class LectureValidator {
public void validate(LectureDto lectureDto, Errors errors) {
LocalDateTime endDateTime = lectureDto.getEndDateTime();
if (endDateTime.isBefore(lectureDto.getBeginDateTime())) {
errors.rejectValue("endDateTime", "wrongValue", "EndDateTime is wrong");
}
}
}
Controller (이건 그냥 Controller에서 /api/lectures 부분과 제일밑에 return 으로 created하는 부분만 확인)
@Controller
@RequestMapping(value = "/api/lectures", produces = MediaTypes.HAL_JSON_VALUE)
public class LectureController {
private final LectureRepository lectureRepository;
private final ModelMapper modelMapper;
private final LectureValidator lectureValidator;
public LectureController(LectureRepository lectureRepository, ModelMapper modelMapper, LectureValidator lectureValidator) {
this.lectureRepository = lectureRepository;
this.modelMapper = modelMapper;
this.lectureValidator = lectureValidator;
}
@PostMapping
public ResponseEntity createLecture(@RequestBody @Valid LectureDto lectureDto, Errors errors) {
if(errors.hasErrors()){
return ResponseEntity.badRequest().body(errors);
}
lectureValidator.validate(lectureDto, errors);
if(errors.hasErrors()){
return ResponseEntity.badRequest().body(errors);
}
Lecture lecture = modelMapper.map(lectureDto, Lecture.class);
lecture.update();
Lecture newLecture = this.lectureRepository.save(lecture);
WebMvcLinkBuilder selfLinkBuilder = linkTo(LectureController.class).slash(newLecture.getId());
URI createdUri
= selfLinkBuilder.toUri();
LectureResource lectureResource = new LectureResource(lecture);
lectureResource.add(linkTo(LectureController.class).withRel("query-lectures"));
lectureResource.add(selfLinkBuilder.withSelfRel());
lectureResource.add(selfLinkBuilder.withRel("update-lecture"));
return ResponseEntity.created(createdUri).body(lectureResource);
}
}
우선 이정도로 정리 하겠다.
강의에 대한 등록에 관해 만든 코드 들인데 .
강의의
이름
설명
개강
종강
장소
가격
온라인(boolean)
무료(boolean)
정도만 보면 되겠다.
기본적으로 우리가 해볼 test는 POST Mapping 시에 Validate를 검사하는 것이다.
예를들어
1. 정상적으로 모든값을 잘 넣어줬을 때 .
2. 입력 값이 정상적이지 않은 경우.
TDD예시
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class LectureControllerTests {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
//본격적인 시작
@Test
@TestDescription("정상적으로 강의를 생성하는 테스트")
public void createLecture() throws Exception {
LectureDto lecture = LectureDto.builder()
.name("Spring")
.description("REST API Developmetn with Spring")
.beginEventDateTime(LocalDateTime.of(2020, 09, 28, 14, 00))
.endEventDateTime(LocalDateTime.of(2020, 11, 28, 16, 00))
.price(150000)
.location("무식한개발자 집앞 카페")
.build();
mockMvc.perform(post("/api/lectures") //post Method를 사용한다.
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaTypes.HAL_JSON)
.content(objectMapper.writeValueAsString(lecture))) //위에 @Autowired된 ObjectMapper objectMapper 확인 할 것.
.andDo(print())
.andExpect(status().isCreated()) //isCreated == is(201) 같은 내용
.andExpect(jsonPath("id").exists())
.andExpect(header().exists(HttpHeaders.LOCATION)) // "location"보다 Type-Safe한 방식
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE))//"Content_Type", "application/hal+json" 보다 Type-Safe한 방식
.andExpect(jsonPath("id").value(Matchers.not(100)))
.andExpect(jsonPath("free").value(false))
.andExpect(jsonPath("offline").value(false))
;
}
}
우리는 실제 repository 를 만들어서 사용할 것이기 때문에 굳이
@MockBean
LectureRepository lectureRepository
를 사용하지 않았다.
간단하게 생각하면 된다.
우리가 LectureDto에 builder 패턴으로 각각에 해당되는 값들을 넣고
그 값들이 exists하고 boolean들의 value가 무엇인지 써주고 이것들이 일치하는지 확인하는것 뿐이다.
이것은 가격이 있음에도 불구하고 free가 false가 아닌경우다. 이부분에 대해서는 위에
Entity updateBoolean에 선언해놨다.
@Test
@TestDescription("가격이 있는데 free가 true일 경우")
public void createLecture() throws Exception {
LectureDto lecture = LectureDto.builder()
.name("Spring")
.description("REST API Developmetn with Spring")
.beginEventDateTime(LocalDateTime.of(2020, 09, 28, 14, 00))
.endEventDateTime(LocalDateTime.of(2020, 11, 28, 16, 00))
.price(150000)
.location("무식한개발자 집앞 카페")
.build();
mockMvc.perform(post("/api/lectures") //post Method를 사용한다.
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaTypes.HAL_JSON)
.content(objectMapper.writeValueAsString(lecture))) //위에 @Autowired된 ObjectMapper objectMapper 확인 할 것.
.andDo(print())
.andExpect(status().isBadRequest());
;
}
이렇게 잘못된 값이 들어오면 우리가 기대할수 있는 대답은 BadRequest이다.
때문에
.andExpect(status().isBadRequest());
라고 선언해 주었다.
이런 부분을 잘 확인하고 우리들의 프로잭트에 대한 이해를 높혀야한다.
TDD는 팔수록 어려워진다고 하지만 우선 우리는 간단한 CRUD에 대한 Validate 확인으로 시작하자 .
어떤 값이 들어와야하며 어떤값들을 쳐내야하는지 수많은 경우를 생각하며 TDD를 작성해 나가며
우리들의 본 코드를 수정해 나가야한다.
다들 쉬운것부터 시작해보자 !