단위테스트란?
고전파
모든 사람이 다누이 테스트와 테스트 주도 개발에 원론적으로 접근
런던파
런던의 프로그래밍 커뮤니티에서 시작
단위테스트 특징
- 작은 코드 조각을 검증
- 빠르게 수행
- 격리된 방식으로 처리하는 자동화된 테스트
격리된 방식에 대한 접근법이 런던파와 고전파가 다르다.
테스트 코드 작성 예시
비즈니스 로직
- 고객은 제품을 구매할 수 있다.
- 재고가 충분하다
- 구매가 성공한다
- 제품의 재고가 1개 줄어든다
- 재고가 부족하다
- 구매 실패한다.
- 어떠한 액션도 발생하면 안된다.
- 재고가 충분하다
고전파 테스트 코드
구매 성공
public void purchase_success_when_enough_inventory(){
//given
Store store = new Store() // 상점 생성
store.addInventory("MacBook Pro", 10); // 상점에 맥북 10개 추가
Customer customer = new Customer();
// when
boolean result = customer.purchase(store, "MacBook Pro", 3); // 상점에서 맥북 3개 구매
// then
assertTrue(result);
assertEquals(7, store.getInventory("MacBook Pro")); // 남은 재고 7개
}
구매 실패
public void purchase_fails_when_not_enough_inventory() {
// given
Store store = new Store(); // 상점 생성
store.addInventory(“MacBook Pro”, 10); // 상점에 맥북 10개 추가
Customer customer = new Customer();
// when
boolean result = customer.purchase(store, “MacBook Pro”, 15); // 상점에서 맥북 15개 구매
// then
assertFalse(result); // 구매 실패 (재고 부족)
assertEquals(10, store.getInventory(“MacBook Pro”)); // 남은 재고 그대로 10개
}
특징
객체들을 별도의 테스트 베드로 교체하지 않고 그대로 사용한다. 두개의 객체 모두 테스트가 가능해지지만 두개중 하나의 객체 내에서 버그가 발생하면 해당 단위 테스트는 실패할 수 있다. 왜냐하면 두 클래스는 서로 격리되어 있지 않기 때문이다.
런던파 테스트 코드
Mocking 사용
@Mock Store store;
public void purchase_success_when_enough_inventory() {
// given
given(store.hasEnoughInventory(“MacBook Pro”)).willReturn(true); // 해당 메소드가 호출되면 true 를 반환
Customer customer = new Customer();
// when
boolean result = customer.purchase(store, “MacBook Pro”, 3); // 상점에서 맥북 3개 구매
// then
assertTrue(result); // 구매 성공
verify(store.removeInventory(“MacBook Pro”, 3), times(1)) // 맥북 3개 재고 감소 메소드 1번 호출
}
Mocking의 의미?
Store라는 객체를 그대로 사용하지 않고 임의의 어떤 클래스로 대체한다는 의미.
특징
- .willReturn(true); 재고에 상관없이 호출만 되면 true 리턴
- times(1) 무조건 1번 호출되는지 확인
테스트 하고자 하는 비즈니스 로직에만 집중하여 테스트 코드를 작성
런던파의 접근
런던파에서는 테스트 대상 시스템을 협력자에게서 격리하는 것을 의미한다. 하나의 클래스가 다른 클래스 또는 여러 클래스에 의존하면 이 모든 의존성을 테스트 대역으로 대체해야한다. 외부 영향과 분리해서 테스트 대상 클래스에만 집중할 수 있도록 한다.
단위 테스트의 목표
소프트웨어의 지속 가능한 성장을 가능하게 하기 위해 단위테스트를 작성한다. 단위 테스트를 작성하다보면 더 나은 코드 설계로 이어질 수 있다. 하지만 작업 소요시간이 테스트가 없을 때 보다 훨씬 높아지게 된다. 하지만 프로젝트가 후반으로 갈 수록 테스트가 없는 프로젝트는 작업 소요시간이 수직으로 상승하고 테스트를 포함한 프로젝트는 완만히 상승한다.
좋은 테스트와 좋지 않은 테스트
모든 테스트를 작성할 필요는 없으며, 잘못퇸 테스트는 오류를 알아내는데 도움이 되지 않고, 유지보수가 어려워지며 리소스만 투자하게 되는 경우가 발생할 수 있다. 잘못된 테스트를 포함한다면 작업 소요시간은 테스트가 없는 테스트 만큼은 아니지만 비슷한 수준으로 상승할 수 있다.
성공적인 테스트
- 개발 주기에 통합되어 있어야한다.
- 코드가 변경될 때마다 테스트 코드를 실행하여 끊임없는 테스트를 하는 것이 좋다
- 코드 베이스에서 가장 중요한 부분만을 대상으로 한다.
- 도메인 모델, 비즈니스 로직을 위주로 테스트 코드를 작성한다.
- 최소 유지비로 최대 가치를 끌어낸다
- 가치있는 테스트를 식별하기 + 가치있는 테스트를 작성하기
단위테스트의 구조
Given-When-Then 패턴
- Given : 준비
- When : 실행
- Then : 검증
아래 코드를 검증해보자
- 전달받은 두 파라미터를 더해 해당 값을 반환
public class Calculator {
pubblic int sum(int a, int b) {
return a + b;
}
}
여기서 무엇을 검증해야할까?
- 3과 5가 전달 되었을 때 8이 반환 되는가?
- -100과 100이 전달되었을 때 0이 반환되는가?
- 0과 0이 전달되었을 때 0이 반환 되는가?
즉 두 파라미터가 전달되었을 때 기대하는 값이 반환되는가?
검증
public class CalculatorTests {
public void sum_of_two_numbers() {
// given
int a = 3;
int b = 5;
Calculator calculator = new Calculator();
// when
int actual = calculator.sum(a, b); // 무엇이 반환될 지 모르지만 sum 메소드에서 반환되는 값은 actual 에 저장됨
// then
int expected = 8; // 예상값
assertEquals(expected, actual); // 예상값과 실제값이 동일함을 검증
}
단위 테스트 할때 주의사항
- 여러개의 준비, 실행, 검증 구절 피하기
- eg. given-when1-then1-when2-then2
- 실행이 하나가 되면 간단하고, 빠르고, 이해하기 쉽다
- 테스트내 if문 피하기
- 테스트 내에 if문이 들어가야 하는 경우는 여러 테스트로 나누어서 진행한다
- 테스트 내에 분기가 생기면 유지보수 비용이 급격하게 상승한다.
- 실행구절이 한줄 이상인 경우를 조심
- 보통 실행 구절은 한줄!
- 만약 두줄 이상이 필요하다면 코드 리펙토링이 필요할 수 있다.
- 아래는 puchase 메소드에 재고를 감소하는 코드를 포함시켜야 하는 경우
public void purchase_success_when_enough_inventory() { // given Store store = new Store(); store.addInventory(“MacBook Pro”, 10); Customer customer = new Customer(); // when boolean success = customer.purchase(store, ”MackBook Pro”, 5); // 상품 구매 store.removeInventory(success, “MacBook Pro”, 5); // 재고 5개 감소 // then assertTrue(success); assertEquals(5, store.getInventory(“MackBook Pro”)); }
- 가독성을 위해서 given-when-then을 적을 것인가?
- 팀 컨벤션에 따르자
좋은 단위 테스트의 4대 요소
- 회귀 방지
- 리팩토링 내성
- 빠른 피드백
- 유지 보수성
회기방지
- 회기버그 → 기존에 제대로 동작하던 소프트웨어 기능에 문제가 생기는 것을 의미(신규기능 추가 또는 기존 기능 수정)
- 코드 베이스가 커질 수록 잠재적인 버그에 더 많이 노출됨
- 이것들을 방지하기 위해서는 테스트가 가능한 많은 코드를 실행하는 것이 중요함
리펙토링 내성
작성해둔 테스트가 실패 되지 않으면서 소프트웨어의 코드를 리팩토링 할 수 있는가?
리펙토링 후 기존 기능이 정상적으로 동작하지만 테스트 코드가 실패하는 상황을 거짓양성(false positive)라고 한다.
테스트 코드에서 검증해야 하는 부분은 입력이 들어왔을 때, 기대하는 출력으로 결과값이 나오는지만 보면 된다. 구현 부분의 흐름까지 체크한다면 순서가 변경 되었을 때 테스트가 깨질 우려가 있다. 쉽게 깨지지 않는 테스트를 만들려면 구현 세부 사항 보다는 최종 결과에 집중해서 테스트를 하자.
거짓 양성의 장단점
장점
- 테스트를 통해 기존 기능에 대한 재확인을 할 수 있도록 유도(조기 경고)
- 코드의 변경으로 인해 회귀 버그가 발생하지 않을 것에 대한 확신
단점
- 타당한 이유 없이 실패한다면 테스트를 고치는것, 기존 코드를 고치는 것에 대해 무감각해짐
- “깨진 유리창 이론”
빠른 피드백과 유지 보수성
테스트 속도가 빠를수록 더 많은 테스트를 수행할 수 있고, 더 자주 실행할 수있다. 높은 유지 보수성을 위해서는 테스트 코드가 이해하기 쉬운가, 테스트를 실행하기 쉬운가를 생각해보자.
이상적인 테스트는 없다!
리팩토링 내성과 회귀방지, 빠른 피드백의 교집합을 찾자. 하나의 특성이 0이 되면 전체 테스트의 가치는 0이 된다.
End-to-End 테스트
UI, 데이터베이스, 외부 어플리케이션을 포함한 모든 시스템 구성 요소를 테스트 하는것을 의미한다.
특징
- 많은 코드를 테스트하기 때문에 회귀 방지를 성공적으로 한다.
- 최종 사용자 관점에서 테스트를 진행하기 때문에 세부 구현 사항을 최대한 제거한다. ⇒ 리펙토링 내성도 우수하다
- 테스트가 많기 때문에 피드백이 느릴 수 밖에 없다(빠른 피드백 불가)
간단한 테스트
말 그대로 간단한 테스트
특징
- 빠른 피드백 가능
- 거짓 양성이 생길 가능성이 매우 낮기 때문에 리펙토링 내성 높다
- 기반 코드 베이스에 실수할 가능성이 거의 제로이다
- 회귀 방지를 나타내지 않는다
- 검증이 무의미하다.
깨지기 쉬운 테스트
public String getUserByIdSql(String userId) {
return “SELECT * FROM User where UserId = userId;”
}
public void test_get_user_by_id_sql() {
String sql = sut.getUserByIdSql(“test”);
assertEquals(“SELECT * FROM User where UserId = test;”, sql);
}
위의 코드에서
“SELECT UserId, Name, Email from User where UserId = test”라는 코드를 사용해도 동일한 결과값을 기대할 수는 있다. 하지만 테스트 자체는 실패하게 된다.
현실적으로 보는 이상적인 테스트
리펙토링내성, 회귀방지, 빠른 피드백중에서 리팩토링 내성을 최대화 시키고 나머지 두개는 선택을 하는 방식으로 한다.
'IT 연구소' 카테고리의 다른 글
입사자 과제 회고 (0) | 2024.04.22 |
---|---|
단위테스트 프레임워크(Junit5, Spock) (0) | 2024.03.05 |
유용한 Git 명령어 (0) | 2024.02.16 |
다시 정리하는 Git의 개념 (0) | 2024.02.16 |
자바 웹 현재 사용기술에 대한 고찰 (0) | 2024.01.30 |