일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- skt fellowship 3기
- C++
- google login
- javascript
- oauth
- 양방향 매핑
- 코드업
- google cloud
- matplotlib
- 2021 제9회 문화공공데이터 활용경진대회
- google 로그인
- JPA
- Spring
- react native
- 졸프
- STT
- pandas
- YOLOv5
- 순환참조
- 커스텀 데이터 학습
- idToken
- OG tag
- AWS
- Expo
- marksense.ai
- yolo
- Spring Boot
- Loss Function
- @Transactional
- html
- Today
- Total
민팽로그
[JUnit]단위 테스트와 통합 테스트 본문
테스트의 필요성
개발을 하다보면 예상치 못한 버그가 끊임없이 나타난다. 버그를 잡아내지 못한 채 서비스를 운영하게 된다면 서비스 이용자들에게 불편함을 주며 서비스 운영사에 악영향을 끼친다. 따라서 코드를 배포하기 전 충분히 테스트 하여 버그를 잡아내야 한다.
블랙박스 테스트 & 개발자 테스트
1. 블랙박스 테스트
소프트웨어의 내부 구조나 동작 원리를 모르는 상태에서 서비스의 동작을 테스트 하는 방법으로 동치 분할 검사, 경계값 분석, 원인-효과 그래프 검사 등이 있다(자세한 내용 정보처리기사 책 참고하여 정리해보기).
개발자부터 디자이너, 일부 베타 테스터까지 누구나 테스트가 가능하다는 장점이 있지만, 서비스가 커져 기능이 증가할수록 테스트 범위도 함께 커지기 때문에 테스트 비용이 늘어난다는 단점이 있다.
2. 개발자 테스트
개발자 본인이 작성한 코드를 본인이 직접 테스트 하는 것이다. 예상 동작과 실제 동작을 비교하여 빠르고 정확한 테스트가 가능하며 테스트 자동화가 가능하고 배포 시 항상 검증 가능하다는 장점이 있다. 하지만 테스트 코드를 작성하기 위한 개발 시간이 오래 걸리고 유지보수 비용이 든다는 단점이 있다.
테스트 코드를 작성하기 위해 시간과 노력이 필요하지만 한번 테스트 코드를 작성해 놓으면 기능이 많이 추가되어도 언제든지 쉽고 빠르게 테스트를 할 수 있다. 스프링은 테스트 코드를 작성하기 위한 환경을 제공해주기 때문에 단위 테스트를 좀 더 효율적으로 할 수 있다.
단위 테스트
프로그램을 작은 단위로 쪼개어 각 단위가 정확하게 동작하는지 검사하는 것으로, 단위 테스트를 통해 문제가 발견됐다면 정확하게 어느 부분이 잘못되었는지 확인할 수 있다.
TDD(Test-Driven Development)
AS-IS) 설계 → 개발 → 테스트 (→ 설계 수정) 순서
TO-BE) 설계 → 테스트 (→설계 수정) → 개발
설계 > 테스트 & 설계 수정 > 개발 의 절차를 반복하며 개발하는 방법이다.
JUnit
JUnit? 자바 프로그래밍 언어용 단위 테스트 프레임워크이다. 다양한 언어의 테스트 프레임워크를 나타내는 XUnit에서 자바를 사용하기 때문에 JUnit이다.
spring boot 프로젝트의 build.gradle 파일을 보면 자동으로 JUnit 사용을 위한 환경이 아래와 같이 셋팅되어있다.
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
인텔리제이에서 테스트를 원하는 클래스 파일 내에서 마우스 오른쪽 버튼 클릭 > Generate... > Test... 를 차례로 클릭하면 테스트 코드를 위한 클래스가 생성된다.
(Junit5 기준)
예제 코드 1
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class BookTest {
@Test
@DisplayName("정상 케이스")
void createBook_Normal() {
// given
Long userId = 100L;
String title = "안녕하세요";
BookRequestDto requestDto = new BookRequestDto(
title
);
// when
Book book = new Book(requestDto, userId);
// then
assertNull(book.getId());
assertEquals(userId, book.getUserId());
assertEquals(title, product.getTitle());
}
}
@Test : @Test 어노테이션을 활용하여 테스트를 위한 단위 함수를 설정해줄 수 있다.
@DisplayName : 테스트 결과창에 나타날 이름을 설정할 수 있다.
assertNull : 파라미터에 주어진 값이 null인지 검사한다. null이 아니면 테스트 실패
assertEquals : 첫번째 인자가 두번째 인자와 같은 값을 갖는지 검사한다. 다르다면 테스트 실패
테스트 코드는 기본적으로 given, when, then으로 나누어 작성하게 된다. 어떤 조건이 주어질 때(given) 어떤 일이 벌어진다면(when) 어떤 결과가 나오는지(then)에 대한 코드를 작성해 준다.
예제코드 2 (경계값 분석, Edge 케이스를 고려한 단위 테스트)
import static org.junit.jupiter.api.Assertions.*;
class ProductTest {
@Nested
@DisplayName("회원이 요청한 관심상품 객체 생성")
class CreateUserProduct {
private Long userId;
private String title;
private String image;
private String link;
private int lprice;
@BeforeEach
void setup() {
userId = 100L;
title = "오리온 꼬북칩 초코츄러스맛 160g";
image = "https://shopping-phinf.pstatic.net/main_2416122/24161228524.20200915151118.jpg";
link = "https://search.shopping.naver.com/gate.nhn?id=24161228524";
lprice = 2350;
}
@Test
@DisplayName("정상 케이스")
void createProduct_Normal() {
// given
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when
Product product = new Product(requestDto, userId);
// then
assertNull(product.getId());
assertEquals(userId, product.getUserId());
assertEquals(title, product.getTitle());
assertEquals(image, product.getImage());
assertEquals(link, product.getLink());
assertEquals(lprice, product.getLprice());
assertEquals(0, product.getMyprice());
}
@Nested
@DisplayName("실패 케이스")
class FailCases {
@Nested
@DisplayName("회원 Id")
class userId {
@Test
@DisplayName("null")
void fail1() {
// given
userId = null;
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
new Product(requestDto, userId);
});
// then
assertEquals("회원 Id 가 유효하지 않습니다.", exception.getMessage());
}
@Test
@DisplayName("마이너스")
void fail2() {
// given
userId = -100L;
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
new Product(requestDto, userId);
});
// then
assertEquals("회원 Id 가 유효하지 않습니다.", exception.getMessage());
}
}
@Nested
@DisplayName("상품명")
class Title {
@Test
@DisplayName("null")
void fail1() {
// given
title = null;
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
new Product(requestDto, userId);
});
// then
assertEquals("저장할 수 있는 상품명이 없습니다.", exception.getMessage());
}
@Test
@DisplayName("빈 문자열")
void fail2() {
// given
String title = "";
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
new Product(requestDto, userId);
});
// then
assertEquals("저장할 수 있는 상품명이 없습니다.", exception.getMessage());
}
}
}
}
}
@Nested : 클래스 내에 하위 계층을 만들 수 있으며 테스트들을 구조적으로 정리할 수 있다.
@BeforeEach : 각각의 테스트가 수행되기 전 먼저 실행되는 내용으로, 전체 테스트들이 공통적으로 사용할 내용을 작성해주고 각 테스트에서 변경이 필요하다면 값을 변경하여 사용하
Mock Object
위키백과 정의: 주로 객체 지향 프로그래밍으로 개발한 프로그램을 테스트 할 경우 테스트를 수행할 모듈과 연결되는 외부의 다른 서비스나 모듈들을 실제 사용하는 모듈을 사용하지 않고 실제의 모듈을 "흉내"내는 "가짜" 모듈을 작성하여 테스트의 효용성을 높이는데 사용하는 객체이다. 사용자 인터페이스(UI)나 데이터베이스 테스트 등과 같이 자동화된 테스트를 수행하기 어려운 때 널리 사용된다.
Mock object는 실제 객체와 겉만 같은 객체로 동일한 클래스명과 함수명을 갖고 있지만 내부 로직은 같지 않다. 실제 서비스로 운영될 객체들을 가지고 테스트 하지 않기 위해 사용한다. 개발자가 mock object 함수를 테스트 시나리오 별로 설정 가능하다.
- 입력1 → 출력1
- 입력2 → 출력2
mock object를 직접 구현해도 되지만, 프레임워크를 사용하면 쉽게 구현이 가능하다.
Mockito 프레임워크는 Mock object를 쉽게 만들 수 있는 방법을 제공한다.
예제 코드: BookServiceTest.java
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class BookServiceTest {
@Mock
BookRepository bookRepository;
@Test
@DisplayName("updateBook() 에 의해 책 가격이 3만원으로 변경되는지 확인")
void updateBook_Normal() {
// given
Long bookId = 100L;
int price = 30000;
BookPriceRequestDto requestPriceDto = new BookPriceRequestDto(
price
);
Long bookId = 12345L;
BookRequestDto requestBooktDto = new BookRequestDto(
"안녕하세요",
2350
);
Book book = new Book(requestBookDto, bookId);
BookService bookService = new BookService(bookRepository);
when(bookRepository.findById(bookId)) //bookRepository.findById(bookId)일 때
.thenReturn(Optional.of(book)); //반환해야 할 값
// when
Book result = bookService.updateBook(bookId, requestPriceDto);
// then
assertEquals(price, result.getPrice());
}
@Test
@DisplayName("updateBook() 에 의해 가격이 100원 이하인 경우 에러 발생")
void updateBook_abnormal() {
// given
Long bookId = 100L;
int price = 50;
BookPriceRequestDto requestPriceDto = new BookPriceRequestDto(
price
);
Long bookId = 12345L;
BookRequestDto requestBookDto = new BookRequestDto(
"안녕하세요",
2350
);
Book book = new Book(requestBookDto, bookId);
BookService bookService = new BookService(bookRepository);
when(bookRepository.findById(bookId))
.thenReturn(Optional.of(book));
// when
// then
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
bookService.updateBook(bookId, requestBookDto);
});
}
}
@ExtendWith : 확장 기능을 사용하기 위한 어노테이션이다. 클래스 위에 @ExtendWith(MockitoExtension.class) 를 붙여주면 Mockito의 기능을 사용할 수 있다.
@Mock : 객체를 Mock Object로 설정해준다. Mock Object를 사용하기 위해서는 객체를 생성하고 끝인 것이 아니라 어떤 입력이 들어왔을 때 어떤 결과가 나와야 하는지를 개발자가 코딩해주어야 한다. -> Mockito 프레임워크의 when()과 thenReturn() 사용
통합 테스트
여러 단위 테스트를 하나의 통합하여 테스트를 수행하는 것.
예제 코드
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) //랜덤 포트 사용
@TestInstance(TestInstance.Lifecycle.PER_CLASS) //클래스 단위의 생명주기를 갖음
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class ProductIntegrationTest {
@Autowired
ProductService productService;
Long userId = 100L;
Product createdProduct = null;
int updatedMyPrice = -1;
@Test
@Order(1)
@DisplayName("신규 관심상품 등록")
void test1() {
// given
String title = "갤럭시";
String imageUrl = "이미지URL";
String linkUrl = "linkURL";
int lPrice = 77000;
ProductRequestDto requestDto = new ProductRequestDto(
title,
imageUrl,
linkUrl,
lPrice
);
// when
Product product = productService.createProduct(requestDto, userId);
// then
assertNotNull(product.getId());
assertEquals(userId, product.getUserId());
assertEquals(title, product.getTitle());
assertEquals(imageUrl, product.getImage());
assertEquals(linkUrl, product.getLink());
assertEquals(lPrice, product.getLprice());
assertEquals(0, product.getMyprice());
createdProduct = product;
}
@Test
@Order(2)
@DisplayName("신규 등록된 관심상품의 희망 최저가 변경")
void test2() {
// given
Long productId = this.createdProduct.getId();
int myPrice = 70000;
ProductMypriceRequestDto requestDto = new ProductMypriceRequestDto(myPrice);
// when
Product product = productService.updateProduct(productId, requestDto);
// then
assertNotNull(product.getId());
assertEquals(userId, product.getUserId());
assertEquals(this.createdProduct.getTitle(), product.getTitle());
assertEquals(this.createdProduct.getImage(), product.getImage());
assertEquals(this.createdProduct.getLink(), product.getLink());
assertEquals(this.createdProduct.getLprice(), product.getLprice());
assertEquals(myPrice, product.getMyprice());
this.updatedMyPrice = myPrice;
}
@Test
@Order(3)
@DisplayName("회원이 등록한 모든 관심상품 조회")
void test3() {
// given
int page = 0;
int size = 10;
String sortBy = "title";
boolean isAsc = false;
// when
Page<Product> productList = productService.getProducts(userId, page, size, sortBy, isAsc);
// then
// 1. 전체 상품에서 테스트에 의해 생성된 상품 찾아오기 (상품의 id 로 찾음)
Long createdProductId = this.createdProduct.getId();
Product foundProduct = productList.stream()
.filter(product -> product.getId().equals(createdProductId))
.findFirst()
.orElse(null);
// 2. Order(1) 테스트에 의해 생성된 상품과 일치하는지 검증
assertNotNull(foundProduct);
assertEquals(userId, foundProduct.getUserId());
assertEquals(this.createdProduct.getId(), foundProduct.getId());
assertEquals(this.createdProduct.getTitle(), foundProduct.getTitle());
assertEquals(this.createdProduct.getImage(), foundProduct.getImage());
assertEquals(this.createdProduct.getLink(), foundProduct.getLink());
assertEquals(this.createdProduct.getLprice(), foundProduct.getLprice());
// 3. Order(2) 테스트에 의해 myPrice 가격이 정상적으로 업데이트되었는지 검증
assertEquals(this.updatedMyPrice, foundProduct.getMyprice());
}
}
@SpringBootTest : 스프링 부트가 제공하는 테스트 어노테이션으로, 테스트 수행 시 스프링이 동작한다. 따라서 Spring IoC를 사용할 수 있고, Repository를 사용하여 DB CRUD 작업도 가능하다.
End to End 테스트도 가능! -> Client 요청 → Controller → Service → Repository → Client 응답
@Order(1), @Order(2), ... : 테스트의 순서를 정할 수 있다.