1. 간단한 회원 관리 프로그램을 만들어 테스트 해보자.
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class MemoryMemberRepositoryTest { // 잘 작동하는지 테스트
MemberRepository repository = new MemoryMemberRepository();
@Test
public void save() {
Member member = new Member();
member.setName("커비");
/**
* 위 new 키워드로 꺼낸 데이터와 DB에서 꺼낸 데이터가 일치한다 -> true
*/
repository.save(member);
Member result = repository.findById(member.getId()).get(); // 반환 타입 Optional -> 값을 꺼낼 때 get으로 꺼냄 -> get으로 꺼내는 것이 좋은 방법은 아니지만 Test에 한정해 그냥 사용한다.
Assertions.assertEquals(member, result); // 만약 member와 result가 같을 때 실행하면 글자가 출력되진 않지만 콘솔에 초록색 체크표시가 뜬다.
}
}
왼쪽에 보이는 초록색 체크표시! 글자는 출력되지 않지만 정상적으로 작동하고 있다는 증거이다.
만약 Assertions 안에 result 값을 다른 것으로 바꾸면
Assertions.assertEquals(member, null);
Expected 에서 나와야 할 값과 들어온 Actual 값이 다르다며 오류가 발생한다.
위 Assertions.assertEquals 도 있지만 또 다르게 값을 대조하는 방법이 있다.
assertThat(member).isEqualTo(result); // Assertions 를 입력하고 static import를 하면 assertThat 만 간단하게 입력해 member와 result를 비교할 수 있다.
코드 자체가 영어 순서대로 입력되어 있어 한 눈에 봐도 member와 result가 같은 지 비교한다는 것을 알 수 있어 편리하다.
또 한 번만 Assertions static import를 해두면 이후부턴 assertThat만 입력해도 된다.
@Test
public void findByName() {
Member member1 = new Member(); // member1, member2 회원이 가입이 되었다고 가정
member1.setName("커비2");
repository.save(member1);
Member member2 = new Member();
member2.setName("커비3");
repository.save(member2);
//findByName이 잘 작동하는지 확인
Member result = repository.findByName("커비2").get();
assertThat(result).isEqualTo(member1);
}
}
member1과 member2를 새로운 회원이 가입을 한 것이라고 가정하고 회원이 더 늘어났을 때 findByName() 메서드가 잘 작동하는 지 보기 위한 테스트를 진행했다. findByName에 커비 2 라고 입력해놓고 isEqualsTo() 에는 member2 라고 해놔서 에러가 났었다.
각 member객체마다 주어진 name이 같은 지 꼭 확인해서 당황하지 않도록 하자.
@Test
public void findAll() { //멤버들이 리스트로 반환됨
Member member1 = new Member();
member1.setName("커비2");
repository.save(member1);
Member member2 = new Member();
member2.setName("커비3");
repository.save(member2);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(2); //리스트에 멤버 데이터 2 개를 넣었으니 조건 true로 초록체크가 나온다.
}
}
member1과 member2가 List에 들어있다고 가정하고 TEST를 했다. findAll() 메서드를 이용했는데 isEqualsTo() 안에 2가 아니라 3 혹은 다른 숫자를 넣으면 에러가 발생한다. 멤버 객체들의 목록(리스트)의 사이즈가 2와 같냐고 물어보는 것이라 리스트 안에 있는 객체는 member1과 member2 2개 뿐이므로, 다른 숫자를 입력하면 에러가 발생하는 것이다.
잠깐! 이 TEST에서 우리는,
각 TEST 애너테이션 마다 TEST를 독립적으로 진행할 수 있고

class 단위로 TEST를 한 번에 진행할 수도 있다.
TEST마다 Run 버튼이 따로 있어 궁금해져서 질문을 했는데, TEST는 해당 애너테이션 내에 있는 상황들을 독립적으로 생각하는 것이라, 여러 상황을 가정하고 따로 TEST 를 하기에 편리하겠구나 라고 생각했다.
그런데 모두 정상적으로 작동되었던 TEST가 클래스 단위로 TEST를 진행하니 갑자기 에러가 났다.
그 이유는 클래스 단위로 TEST를 진행하면 무작위 순서로 TEST 가 진행되기 때문에 findAll, findByName, save 순서로 진행되어 에러가 난 것이다. 모든 TEST는 순서와 상관없이 메서드 별로 따로 동작하게 설계를 해야한다.(순서에 의존하게끔 설정하면 안된다)
findAll() 메서드부터 진행되었기 때문에 이미 다른 객체 데이터가 저장되어 findByName에서 에러가 난 것이다.
그래서 하나의 TEST를 진행하고 나면 데이터를 깔끔하게 정리해줘야 한다.
그럴 때 사용하는 것이 afterEach(), 콜백 메서드이다.
각 메서드들이 실행을 마칠 때마다 다시 afterEach() 메서드로 돌아오게 하는 것이다.
class MemoryMemberRepositoryTest { // 잘 작동하는지 테스트
MemoryMemberRepository repository = new MemoryMemberRepository(); // MemberRepository 는 인터페이스, MemotyMemberRespository 는 인퍼페이스를 구현하는 클래스.
@AfterEach
public void afterEach() { //각 메서드들이 실행을 마칠 때마다 어떠한 동작을 하게 한다.(콜백 메서드)
repository.clearStore();
}
우선 MemberRepository 인터페이스로 생성했던 생성자를 MemberRepository 클래스 생성자로 변경해준다.
즉, 인터페이스 대신 구현체를 직접 사용해 MemoryMemberRepository 의 구체적인 필드나 메서드를 더 쉽게 호출할 수 있다.
→ MemberRepository 인터페이스 대신, 구현체인 MemoryMemberRepository를 직접 선언
@AfterEach
public void afterEach() { //각 메서드들이 실행을 마칠 때마다 어떠한 동작을 하게 한다.(콜백 메서드)
repository.clearStore();
}
그 다음 afterEach() 메서드에 @AfterEach 애너테이션을 추가해 선언함으로써 각 TEST 메서드가 끝난 뒤 실행될 메서드를 선언해줬다.
각 메서드들이 실행이 끝나면 자동으로 afterEach() 메서드가 실행되어 데이터가 클리어된다.
코드를 위와 같이 수정해주고 나면 클래스 단위로 TEST를 무작위 진행해도 기존에 진행한 TEST에서 저장된 데이터 때문에 에러가 나지 않고 정상적으로 작동된다.
서술한 TEST 방법 외에도 TEST 클래스를 먼저 작성한 후 나머지 구현 클래스들을 작성하는 테스트 주도 개발 (TDD) 을 해서 검증을 진행할 수도 있고, 위 사진과 같이 클래스 단위 TEST를 진행하기 위해 디렉토리를 마우스 오른쪽 버튼으로 클릭해 TEST를 한 번에 자동으로 진행되게 할 수도 있다.
<자세한 설명>
@Test
public void findByName() {
Member member1 = new Member(); // member1이라는 이름의 새로운 Member 회원 객체 생성
member1.setName("커비2"); // member1의 이름을 커비2라고 설정
repository.save(member1); // member1 객체를 resporitory에 저장
Member member2 = new Member();
member2.setName("커비3");
repository.save(member2);
/**
* 반환값은 Optional<Member> 타입이며, .get() .메서드를 사용해 실제 Member 객체를 가져온다.
* 검색 결과는 result라는 변수에 저장된다.
*/
Member result = repository.findByName("커비2").get(); //저장된 회원 중에 이름이 커비2인 회원 검색
assertThat(result).isEqualTo(member1); // 결과 검증 : result와 member1이 같은지 확인한다. assertThat은 테스트에서 검증에 사용하는 메서드로, result와 member1이 같은 경우 테스트가 성공한다.
}
<테스트의 의도>
- findByName(), findAll(), save() 메서드가 각각의 역할에 맞게 잘 작동하는지 검증
- 테스트가 성공하면 각 메서드가 제대로 동작한다는 것을 보장하고 실패하면 저장소나 기능에 문제가 있을 가능성을 알 수 있다.
Opinion
스프링부트 공부를 시작한 지가 벌써 3일이 되었다. 여전히 강의를 들어도 완전히 이해된다는 끄덕임은 잘 나오지 않고있고, 자바 문법을 공부할 때처럼 확실하게 지식을 늘리고 있다는 느낌이 전혀 들지 않는다. 하지만 이제 내가 작성하는 코드가 무엇을 위한 것인지 알고, 현재 내가 뭘 하고 있는 지는 안다. 뭐든 의도를 파악하는 것이 중요한 것같다. TEST클래스를 왜 작성할까? 당연히 내가 구현한 클래스와 메서드가 정상적으로 작동하고 구현되었는지 검증하려고 작성하는 것이다. 그럼 검증을 하려면 어떻게 해야할까? 검증을 위한 메서드를 생성하고 실행해보면 된다. 강의에서 나온 것을 그대로 따라치기보단 객체와 변수 이름도 바꿔보고, 데이터에 다른 값을 넣어 에러도 내보면서 작동 원리를 파악하고 현재 하고 있는 행동이 무엇을 위한 것인지 이해하는 것이 중요하다고 생각되었다.
전에 챗GPT를 쓸 때는 코드를 어떻게 작성해야할 지 몰라 사용했었는데 지금은 강의를 따라 작성한 코드 한 줄 한 줄이 어떤 의미인지 해석해보고 내가 이해한 내용이 맞는지 확인하기 위해 사용하고 있다. 그럼 이해한 내용이 맞는지 확인도 가능하고, 부족한 설명을 채워주기도 해서 유용하다.
시작이 반인데 나는 벌써 반을 넘어서고 있다. 앞으로도 화이팅!!!
'내일배움캠프 > TIL' 카테고리의 다른 글
[Spring_4기 본캠프] Spring 입문 - 스케줄러 실습과 트러블슈팅 | Day 34 (0) | 2024.12.03 |
---|---|
[Spring_4기 본캠프] Spring 입문 - 회원관리 서비스 개발 | Day 33 (0) | 2024.12.02 |
[Spring_4기 본캠프] Spring 입문 - 백엔드 개발 | Day 31 (1) | 2024.11.29 |
[Spring_4기 본캠프] Spring 입문 - build terminal 오류 트러블 슈팅 | Day 30 (3) | 2024.11.28 |
[Spring_4기 본캠프] 상속 & 다형성과 캐스팅 | Day 29 (0) | 2024.11.27 |