Skip to content

Commit

Permalink
README 수정
Browse files Browse the repository at this point in the history
  • Loading branch information
unanchoi committed May 9, 2024
1 parent adb284b commit d59b3b4
Showing 1 changed file with 1 addition and 343 deletions.
344 changes: 1 addition & 343 deletions spring-code-for-deploy/README.md
Original file line number Diff line number Diff line change
@@ -1,343 +1 @@
# Test 코드 스터디 정리


### Test Code를 작성하는 이유

#### Test Tool
- [JUnit5](https://junit.org/junit5/docs/current/user-guide/)
- [AssertJ](https://assertj.github.io/doc/)
- [Mockito](https://site.mockito.org/)

### 기본적인 Test 골격
- BDD 기반으로 시나리오를 정의하여 Test => given, when, then에 맞춰서 Test 작성함.

```Java

@Test
@DisplayName("")
void test() throws Exception {

}
```

#### Spring Boot에서의 Test
- Spring Boot Test에서는 기본적으로 `@SpringBootTest`를 사용하여 테스트를 작성함.


### Layered Architecture 정리

![스크린샷 2024-05-09 오후 4 16 48](https://github.com/DO-SOPT-SERVER/unan/assets/81692211/7ec3aabe-d761-40da-8ab1-f1156642d1da)


## Entity

### Entity 구현

```java
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseTimeEntity {

private static final int MAX_AGE = 100;
private static final int MAX_LENGTH = 12;
private static final short CURRENT_GENERATION = 34;
private static final Pattern NAME_PATTERN = Pattern.compile("가-힣");

@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
private String name;
private String nickname;
private int age;
private boolean isDeleted = false;

@Embedded
private SOPT sopt;

@OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
private final List<Post> posts = new ArrayList<>();

@Builder
private Member(String name,
String nickname,
int age,
SOPT sopt) {
validateAge(age);
validateName(name);
validateNickname(nickname);
this.name = name;
this.nickname = nickname;
this.age = age;
this.sopt = sopt;
this.isDeleted = false;
}

private void validateAge(final int age) {
if (0 > age || age > MAX_AGE) {
throw new MemberException("회원의 나이는 0세 이상 100세 이하입니다.");
}
}

// SOPT는 한국인만 가입 가능함.
private void validateName(final String name) {
if (NAME_PATTERN.matcher(name).matches()) {
throw new MemberException("유저의 이름은 한글만 가능합니다.");
}

if (name.length() > MAX_LENGTH) {
throw new MemberException("유저의 이름은 12자를 넘을 수 없습니다.");
}
}

private void validateNickname(final String nickname) {

if (nickname.length() > 8) {
throw new MemberException("유저의 닉네임은 8자를 넘길 수 없습니다.");
}
}

public void remove() {

this.isDeleted = true;
}

public void updateSOPT(SOPT sopt) {
this.sopt = sopt;
}
}
```


## Repository

### Repository Test

Repository Test는 `@DataJpaTest`를 사용하여 작성할 수 있습니다. `@SpringBootTest`는 Application Context를 로드하고, Spring Bean들을 모두 등록한 뒤에 Test를 하기 때문에 속도가 많이 느려집니다.

```java
@DataJpaTest
@ActiveProfiles("test")
public class PostJpaRepositoryTest {

@Autowired
PostJpaRepository postJpaRepository;

@Autowired
MemberJpaRepository memberJpaRepository;

@Test
@DisplayName("사용자 이름으로 작성한 게시글을 모두 조회할 수 있다.")
void findAllByMemberNameIn() {
// given
Member member1 = createMember("오해영");
Member member2 = createMember("또오해영");
memberJpaRepository.save(member1);
memberJpaRepository.save(member2);
Post post1 = createPost("제목1", "내용1", member1);
Post post2 = createPost("제목2", "내용2", member1);
Post post3 = createPost("제목3", "내용3", member2);
postJpaRepository.saveAll(List.of(post1, post2, post3));

// when
List<Post> findPosts = postJpaRepository.findAllByMemberNameIn(List.of("오해영", "또오해영"));

// then
Assertions.assertThat(findPosts)
.extracting("title", "content")
.containsExactlyInAnyOrder(
Assertions.tuple("제목1", "내용1"),
Assertions.tuple("제목2", "내용2"),
Assertions.tuple("제목3", "내용3")
);
}

private Post createPost(String title, String content, Member member) {
return Post.builder()
.title(title)
.content(content)
.member(member)
.build();
}

private Member createMember(String name) {
SOPT sopt = SOPT.builder()
.part(Part.SERVER)
.build();
return Member.builder()
.age(99)
.name(name)
.sopt(sopt)
.nickname("5hae0")
.build();
}


}
```

### Repository를 Mocking 하고 Test 하는 경우

Mocking은 실제 객체가 아닌 가짜 객체를 생성하여 Test를 작성하는 것 또는 그러한 기술을 의미합니다.
- 단위 Test 자체가 독립적이어햐 하는데, 대부분의 코드들은 의존성을 갖고 있기 때문에, Mocking 없이 단위 Test를 작성하게 되면 Test하기가 어려워진다.


MemberRetrieverTest
- Member를 조회해오는 test에서 MemberJpaRepository를 Mocking하여 Test를 작성

```Java
@SpringBootTest
@ActiveProfiles("test")
public class MemberRetrieverTest {

@Autowired
MemberRetriever memberRetriever;

@MockBean
MemberJpaRepository memberJpaRepository;

@Test
void test() throws Exception {
// given
BDDMockito.when(
memberJpaRepository.findById(1L)
).thenReturn(
Optional.ofNullable(Member.builder()
.age(99)
.name("오해영")
.nickname("5hae0")
.build())
);
// when
Member findMember = memberRetriever.findById(1L);

// then
assertThat(findMember)
.extracting("age", "name", "nickname")
.containsExactly(99, "오해영", "5hae0");
}
}
```



## Service Test
Service 계층의 Test는 기본적으로 아래와 같이 Mocking 하여 단위 Test를 작성할 수 있습니다.

```Java
@ExtendWith(MockitoExtension.class)
public class MemberServiceTest {

@InjectMocks
private MemberService memberService;

@Mock
private MemberRegister memberRegister;

@Mock
private MemberRetriever memberRetriever;

@Mock
private MemberUpdater memberUpdater;

@Mock
private MemberRemover memberRemover;


@Test
void getMemberById() {
BDDMockito.given(memberRetriever.findById(1L)).willReturn(
Member .builder()
.age(99)
.name("오해영")
.nickname("5hae0")
.build()
);

Assertions.assertThat(memberService.getMemberById(1L))
.extracting("age", "name", "nickname")
.containsExactly(99, "오해영", "5hae0");
}

}

```

## Controller Test
- Controller 계층 Test를 할 때는 `@WebMvcTest`를 사용하여 Test 할 수 있습니다.
- Service를 Mocking하고 Test

```Java
@WebMvcTest(controllers = MemberController.class)
public class MemberControllerTest extends ControllerTestManager {

@MockBean
private MemberService memberService;
@Test
@DisplayName("신규 회원을 등록한다.")
void createMember() throws Exception {

// given
when(memberService.create(any(MemberCreateRequest.class)))
.thenReturn("/api/member/1");

MemberCreateRequest request = new MemberCreateRequest(
"오해영",
"5hae0",
28,
SOPT.builder()
.part(Part.DESIGN)
.build()
);

// when, then
mockMvc.perform(MockMvcRequestBuilders.post("/api/member")
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isCreated())
.andExpect(MockMvcResultMatchers.header().string("Location", "/api/member/1"))
;
}

@Test
@DisplayName("특정 회원 정보를 조회한다.")
void test() throws Exception {
// given
Member member = Member.builder()
.age(28)
.name("오해영")
.nickname("5hae0")
.sopt(
SOPT.builder()
.part(Part.DESIGN)
.build()
).build();

BDDMockito.given(memberService.getMemberById(1L))
.willReturn(MemberGetResponse.of(member));

// when, then
mockMvc.perform(MockMvcRequestBuilders.get("/api/member/1"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("name").value("오해영"))
.andExpect(MockMvcResultMatchers.jsonPath("age").value(28))
.andExpect(MockMvcResultMatchers.jsonPath("nickname").value("5hae0"))
.andExpect(MockMvcResultMatchers.jsonPath("soptInfo.part").value("DESIGN"))
.andExpect(MockMvcResultMatchers.jsonPath("soptInfo.generation").value(34));

}

}
```



## 통합 테스트
- 각각의 계층별로 Test를 진행했지만, 모의 객체를 사용한 부분도 있고 각각의 계층에서는 문제가 없지만, 실제 동작할 때 문제가 생기는 상황들도 존재한다. 이를 줄이기 위해서 통합 테스트를 작성할 수 있다.

- Request -> Controller -> Service -> Repository -> Service -> Controller -> Response


Test Coverage 확보를 위해서 통합 테스트 또한 작성할 수 있습니다.
# 세미나 준비용 레포

0 comments on commit d59b3b4

Please sign in to comment.