BE/스프링 부트 3

13장 테스트 코드 작성하기

이제하네 2023. 12. 19. 00:07
  • 테스트: 프로그램의 품질을 검증하는 것으로, 의도대로 프로그램이 잘 동작하는지 확인 하는 과정
    • 테스트 도구를 이용해 반복적인 검증 절차를 자동화 가능
    • 다양한 문제를 미리 예방 하고 코드 변경 등으로 인해 발생하는 부작용도 조기 발견 가능
    • 테스트가 통과하면 지속적인 리팩터링으로 코드를 개선
    • 테스트가 실패하면 잘못된 부분을 찾아 고치는 디버깅을 함 
  • 테스트 코드 작성 단계
    1. 예상 데이터 작성하기
    2. 실제 데이터 획득하기
    3. 예상 데이터와 실제 데이터 비교해 검증하기 
  • 테스트 주도 개발(TDD, Test Driven Development)
    • 먼저 테스트 코드를 만든후 이를 통과하는 최소한의 코드부터 시작해 점진적으로 코드를 개선 및 확장해 나가는 개발 방식 

 

테스트 코드 작성하기

  • 테스트 코드 기본 틀 만들기 
    1. 프로젝트의 src/test/java 디렉터리 안에 service 패키지를 생성후 패키지 안에 ArticleServiceTest.java 파일 생성
    2. 테스트할 메서드 위에 @Test 어노테이션 작성
      • @Test 은 해당 메서드가 테스트를 위한 코드라고 선언
    3. 노란줄이 있는 이유는 사용되는 곳이 없다는 의미 
    4. 테스트 코드를 스프링 부트와 연동하기 위해 다음과 같이 설정
      1. ArticleServiceTest 클래스 위에 @SpringBootTest 작성
        • @SpringBootTest는 스프링 부트와 연동해 통합 테스트를 수행하겠다고 선언
        • @SpringBootTest을 사용하면 테스트 코드에서 스프링 부트가 관리하는 다양한 객체를 주입 가능
      2. ArticleServiceTest 클래스를 테스트하기 위해 articleService 객체를 선언하고 외부에서 객체를 주입해야 하므로 @Autowired 도 작성

ArticleServiceTest.java 코드

package com.example.firstproject.service;

import org.junit.jupiter.api.Test; //Test 패키지 임포트
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*; // 앞으로 사용할 수 있는 패키지 임포트

@SpringBootTest // 해당 클래스를 스프링 부트와 연동해 테스트
public class ArticleServiceTest {
	
	@Autowired
	ArticleService articleService; // 객체 주입
	
	@Test //해당 메서드가 테스트 코드임을 선언
	void index() {
		
	}
}

 

 

index() 테스트하기

  • index()를 테스트하는 순서
    1. 예상 데이터 작성하기
      1.  데이터를 Article 객체에 a, b, c에 저장 이때 id 는 Long 타입으로 접미사 L을 붙임
      2. a, b, c 3개의 객체를 Arrays.asList() 메서드를 사용해 ArrayList을 합치고 이를 List<Article>타입의 expected에 저장
        • 더보기
          Arrays.asList() 메서드 입력된 배열 또는 2개 이상의 동일한 타입 데이터를 정적 리스트로 만들어 반환
           정적 리스트는 고정 크기이므로 add() 나 remove() 메서드를 사용할 수 없음
    2. 실제 데이터 획득하기
      1. articleService.index() 메서드를 호출해 그 결과를 List<<Article>타입의 articles 에 받아옴
    3. 예상 데이터와 실제 데이터 비교해 검증하기 
      1. assertEquals(x,y) 메서드를 이용
        • JUnit에서 제공하는 메서드
        • 예상 데이터(x) ,예상 데이터(y)를 비교해 일치하면 테스트를 통과
      2. x에는 예상 데이터를 문자열로 변환한 expected.toString()을 넣고 y에는 실제 데이터를 문자열로 변환한 articles.toString()을 넣음
    4. 테스트 코드를 실행 하면 테스트 통과 실패를 알수 있음
@SpringBootTest // 해당 클래스를 스프링 부트와 연동해 테스트
public class ArticleServiceTest {
	
	@Autowired
	ArticleService articleService; // 객체 주입
	
	@Test //해당 메서드가 테스트 코드임을 선언
	void index() {
		// 1. 예상 데이터
		Article a = new Article(1L , "가가가가" , "1111"); //예상 데이터 객체로 저장
		Article b = new Article(2L , "나나나나" , "2222"); //예상 데이터 객체로 저장
		Article c = new Article(3L , "다다다다" , "3333"); //예상 데이터 객체로 저장
		List<Article> expected = new ArrayList<Article>(Arrays.asList(a,b,c)); // a,b,c 합치기
		
		// 2. 실제 데이터
		List<Article> articles = articleService.index();
		// 3. 비교 및 검증
		
		assertEquals(expected.toString(), articles.toString());
	}
}

테스트 성공
테스트 실패 (c에 들어있는 데이터를 3L -> 4L로 변경)

 

show() 테스트하기

  1. 게시글을 조회하는 show() 메서드를 테스트 하기위해 ArticleServiceTest 코드 맨 아래에 show()테스트 코드 추가
  2. 게시글 조회에 성공하는 경우와 실패하는 경우로 나눠서 테스트 케이스를 작성하기 위해 show()테스트 코드를 복사하고 하나는 show_성공() , show_실패()로 수정
  3. 게시물 성공하는 상황으로는 존재하는 id를 입력해 성공하는 경우를 테스트
    1. show_성공() 메서드 이름을 show_성공_존재하는_id_입력()으로 수정
    2. 실제 데이터를 얻기 위해 articleService.show(id) 메서드를 호출해서 얻은 결과를 article 객체에 저장
    3. 실제 데이터와 예상 데이터를 assertEquals()메서드를 이용해 비교 
  4. 게시글 조회에 실패하는 상황으로는 존재하는 id를 입력해 실패하는 경우로 테스트
    1. show_실패() 메서드 이름을 show_실패_존재하지_않는_id_입력()으로 수정
    2. 실제 데이터를 얻기 위해 articleService,show(id) 메서드를 호출해서 얻은 결과를 article 객체에 저장
    3. 예상 데이터는 존재하지 않는 id인 -1을 조회한다고 가정해 작성하므로 DB에서 조회되는 내용이 없어 null 을 반환할 것으므로 expected 객체에 null을 저장
    4. 비교 및 검증은 assertEquals() 메서드를 이용하지만 실제 데이터와 예상 데이터의 값 null은 toString() 메서드를 호출할 수 없으므로 첫 번째와 두 번째 전달값은 expected와 article을 사용  
  5. 실패 테스트를 실행하면 존재하지 않는 id로 조회했을 때 실제 null을 반환했고 예상 데이터에서도 null을 반환했기 때문에 테스트 통과
	@Test 
	void show_성공_존재하는_id_입력() {
		// 1. 예상 데이터
		Long id = 1L; //예상 데이터 저장
		Article expected = new Article(id,"가가가가","1111"); //예상 데이터 저장
		// 2. 실제 데이터
		Article article = articleService.show(id); //실제 데이터 저장
		// 3. 비교 및 검증
		assertEquals(expected.toString(),article.toString() ); //비교 	
	}
	
	@Test 
	void show_실패_존재하는_id_입력() {
		// 1. 예상 데이터
		Long id = -1L; //예상 데이터 (null)저장
		Article expected = null; //예상 데이터(null) 저장
		// 2. 실제 데이터
		Article article = articleService.show(id); //실제 데이터 저장
		// 3. 비교 및 검증
		assertEquals(expected,article); //비교 
	}

show() 실행 성공

 

 

create() 테스트하기

  1. ArticleServiceTest 코드 맨 아래에 create() 테스트 코드 추가
  2. 게시글 생성에 성공하는 경우와 실패하는 경우로 나눠서 테스트 케이스를 작성하기 위해 create()  테스트 코드를 복사하고 하나는 create_성공() , create_ 실패()로 수정
  3. 게시글 생성에 성공하는 상황은 title 과 content만 있는 dto를 입력한 경우
    1. create_성공() 메서드 이름을 create_성공_title과_content만_있는_dto입력() 으로 수정
    2. 실제 데이터를 얻기 위해 articleService.create(dto)메서드를 호출에 얻은 결과를 article 객체에 저장
    3. 예상 데이터는 사용자가 새 게시물을 생성한 상황을 가정 그런데 id는 DB에서 자동으로 생성되므로 게시물의 내용으로 들어갈 title , content 필드만 선언하고 이를 dto 객체로 생성 그리고 expected 객체에 예상 데이터의 id, title, content 를 저장하는데 id는 필드를 따로 선언하지 않았으므로 자동으로 생성될 4L을 써줌
    4. 실제 데이터를 예상 데이터를 assertEquals() 메서드를 이용해 비교
  4. 게시글 생성에 실패하는 상황은 id가 포함된 dto가 입력된 경우
    1. create_실패() 메서드의 이름을 create_실패_id가_포함된_dto_입력()으로 수정
    2. 실제 데이터를 얻기 위해 articleService.create(dto) 메서드를 호출해 얻은 결과를 article 객체에 저장
    3. 예상 데이터에 id = 4L , title ="라라라라" , content  ="4444" 필드를 선언하고 dto 객체로 생성
    4. 이렇게 게시글을 생성할떄 id를 넣으면 오류가 발생하기 때문에 null이 반환되므로 expected객체에 null을 저장
    5. 비교는 assertEquals() 메서드를 이용하지만 실제 데이터와 예상 데이터의 값 null은 toString() 메서드를 호출할수 없으므로 첫번째와 두번쨰 전달값은 expeted , article을 사용
@Test 
	void create_성공_title과_content만_있는_dto_입력() {
		// 1. 예상 데이터
		String title = "라라라라"; //title 값 임의 배정
		String content = "4444"; // content 값 임의 배정
		ArticleForm dto = new ArticleForm(null , title , content); //dto 생성
		Article expected = new Article(4L , title , content); // 예상 데이터 저장
		// 2. 실제 데이터
		Article article = articleService.create(dto); //실제 데이터 저장
		// 3. 비교 및 검증
		assertEquals(expected.toString(), article,toString()); //비교
	}
	
	@Test 
	void create_실패_id가_포함된_dto_입력() {
		// 1. 예상 데이터
		Long id = 4L; //id 값 임의 배정
		String title = "라라라라"; //title 값 임의 배정
		String content = "4444"; // content 값 임의 배정
		ArticleForm dto = new ArticleForm(id , title , content); //dto 생성
		Article expected = new Article(4L , title , content); // 예상 데이터 저장
		// 2. 실제 데이터
		Article article = articleService.create(dto); //실제 데이터 저장
		// 3. 비교 및 검증
		assertEquals(expected.toString(), article,toString()); //비교
	}

create_성공_title과_content만_있는_dto_입력() 성공
create_실패_id가_포함된_dto_입력 성공

 

여러 테스트 케이스 한 번에 실행하기

  1. ArticleServiceTest 클래스에서 Run을 선택하면 앞에서 하나씩 테스트 할 때는 통과 하였지만 index()가 테스트 통과하지 못함
  2. 이러한 에러가 발생한 이유는 롤백하지 않아서 생긴 현상 
  3. create() 메서드를 테스트할때 insert문을 실행해 새로운 데이터가 추가되서 발생한 현상
  4. 데이터를 조회(Read)하는 테스트를 제외하고 데이터를 생성(Create), 수정(Update), 삭제(Delete)하는 테스트를 할때는 반드시 해당 테스트를 트랜잭션으로 묶어 테스트가 종류한 후에 원래대로 돌아갈수 있도록 롤백 처리하기 위해 메서드 위에 @Transactional 을 추가

트랜잭션 처리 전
트랜잭션 처리한 후

 

※ update , delete 테스트 코드 작성

	@Test 
	@Transactional
	void update_성공_존재하는_id와_title_content가_있는_dto_입력() {
		// 1. 예상 데이터
		Long id = 1L; //id 값 임의 배정
		String title = "가나다라"; //title 값 임의 배정
		String content = "1234"; // content 값 임의 배정
		ArticleForm dto = new ArticleForm(id , title , content); //dto 생성
		Article expected = new Article(id , title , content); // 예상 데이터 저장
		// 2. 실제 데이터
		Article article = articleService.update(id, dto); //실제 데이터 저장
		// 3. 비교 및 검증
		assertEquals(expected.toString(), article.toString()); //비교
	}
	
	@Test 
	@Transactional
	void update_성공_존재하는_id와_title만_있는_dto_입력() {
		// 1. 예상 데이터
		Long id = 1L; //id 값 임의 배정
		String title = "AAAA"; //title 값 임의 배정
		String content = null; // content 값 임의 배정
		ArticleForm dto = new ArticleForm(id , title , content); //dto 생성
		Article expected = new Article(1L , "AAAA" , "1111"); // 예상 데이터 저장
		// 2. 실제 데이터
		Article article = articleService.update(id, dto); //실제 데이터 저장
		// 3. 비교 및 검증
		assertEquals(expected.toString(), article.toString()); //비교
	}
	
	@Test 
	@Transactional
	void update_실패_존재하는_않는_id의_dto_입력() {
		// 1. 예상 데이터
		Long id = -1L; //id 값 임의 배정
		String title = "가나다라"; //title 값 임의 배정
		String content = "1234"; // content 값 임의 배정
		ArticleForm dto = new ArticleForm(id , title , content); //dto 생성
		Article expected = null; // 예상 데이터 저장
		// 2. 실제 데이터
		Article article = articleService.update(id, dto); //실제 데이터 저장
		// 3. 비교 및 검증
		assertEquals(expected, article); //비교
	}
	
	@Test 
	@Transactional
	void delete_성공_존재하는_id_입력() {
		// 1. 예상 데이터
		Long id = 1L; //id 값 임의 배정
		Article expected = new Article(id , "가가가가" , "1111"); //예상 데이터 저장
		// 2. 실제 데이터
		Article article = articleService.delete(id); //실제 데이터 저장
		// 3. 비교 및 검증
		assertEquals(expected.toString(), article.toString()); //비교
	}
	
	@Test 
	@Transactional
	void delete_실패_존재하지_않는_id_입력() {
		// 1. 예상 데이터
		Long id = -1L; //id 값 임의 배정
		Article expected = null; //예상 데이터 저장
		// 2. 실제 데이터
		Article article = articleService.delete(id); //실제 데이터 저장
		// 3. 비교 및 검증
		assertEquals(expected, article); //비교
	}