이번에 우테코 과제를 하면서 단위테스트라는 것을 해보고싶어 정리하게 되었다.
단위테스트 vs 통합테스트
단위테스트는 프로그램의 기본 단위인 모듈을 테스트하는 방법이다 (=모듈 테스트)
- 각 모듈이 제대로 구현되어있고, 정해진 기능을 정확히 수행하는지를 테스트한다.
- 함수단위로 기능을 테스트한다고 볼 수 있다
통합테스트는 프로그램 내의 구성요소들이 함께 작동하는 방식을 다룬다
- 구성요소들을 모았을 때 발생할 수 있는 문제를 식별한다
- 데이터베이스에 연결한다면, 컨트롤러부터 데이터베이스까지 다녀오는 통합적인 기능자체를 테스트한다
이전에 단위테스트를 바라보던 시각
글쓴이는 이전에 JUnit을 이용하여 스프링부트 테스트코드를 작성한 적이 있다. 그래서 mock, Assertion 등 단위테스트와 관련한 용어를 접했고, 사용법을 본 적이 있었다. 하지만 단위테스트의 필요성을 느끼지 못하여 직접 작성해본 적은 없었다. 단위 테스트의 필요성을 느끼지 못한 이유는 다음과 같다.
- api의 기능을 모두 개발한 상태에서는 통합테스트가 기능테스트에 더 적합하다고 생각함. 함수를 하나하나 테스트하는 건 시간낭비인 것 같다
- 함수는 리팩토링할 때마다 변하는데, 변할 때마다 테스트코드에 오류가 발생하여 테스트코드도 고쳐줘야함. 번거롭다
1. 단위테스트는 시간낭비다?
그동안은 객체지향적으로 설계하기보단 빠르게 기능을 만들어 배포를 하고, 프론트측에서 테스트할 수 있게 만드는 게 더 중요하다고 생각했다. 그래서 기능을 빠르게 만든 후, 그 기능을 테스트하고자 하였다. 예를 들면 게시판을 구현할 때 다음과 같은 상황이라고 생각해보자.
[게시판 글쓰기 기능]
controller/service/repository까지 모두 구현하였고, 기능이 잘 돌아가는지 테스트하고싶다
- 글을 쓸 때 작성시간과 제목, 내용이 정확하게 데이터베이스에 잘 저장되는지에 대한 테스트코드를 작성하자!
이렇게 기능을 모두 완성한 상태에서 기능자체를 테스트하려면 글쓰기 service를 호출한 후, 데이터베이스에서 값을 꺼내 원하는 값이 잘 저장되었는지 확인하는 게 가장 정확하고 빠르게 테스트를 할 수 있는 방법이었다.
(그냥 api를 호출하고 데이터베이스에 잘 저장되었는지 확인하는 과정을 테스트코드로 작성한거임)
하지만 단위테스트를 하려면 함수 하나하나를 테스트해야하는데, 글쓰는 과정에서 다양한 함수가 호출될 수 있다. 예를 들면 다음과 같은 함수가 호출될 수 있음
- 필수요소인 제목이 json에 포함되어있는지 확인 (controller)
- 글 객체를 만들어 제목, 글, 작성시간을 저장 (service)
- 데이터베이스에 글 객체를 저장 (repository)
글쓰기의 경우 비즈니스로직이 간단할 수 있지만, 조금 더 복잡해진다면 테스트해야할 함수가 더 늘어난다. 나는 그냥 글이 잘 써지고 데이터베이스에 잘 저장되는지만 궁금한데 굳이 이렇게 함수 하나하나를 테스트해야할 필요성이 느껴지지 않았다.
2. 변하는 함수에 대한 단위테스트를 왜 수행해야하는가?
리팩토링을 하면 하나의 함수가 여러 개로 나뉘어질 수 있고, 두 가지 함수가 하나로 합쳐질 수도 있다.
단위테스트를 함수단위로 진행해야한다면 함수가 변할 때마다 테스트코드도 같이 고쳐줘야한다. 나는 간단한 코드 리팩토링을 했을 뿐인데 테스트코드에서 하나하나 따라가면서 오류를 고쳐주는게 번거로웠다.
학교에서 소프트웨어공학을 배우면서 단위테스트가 가장 중요하다는 걸 알고는 있었다. 하지만 위와같은 이유로 항상 통합테스트만 진행했고, 단위테스트는 거의 진행해본 적이 없다. 통합테스트만 해도 충분히 원하는 테스트를 모두 수행할 수 있었고, 발생할 수 있는 오류를 잡을 수 있었다. 다음은 이전 프로젝트에서 작성한 실제 테스트코드이다.
@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TaskServiceTest {
@Autowired
private TaskService taskService;
@Autowired
private MemberRepository memberRepository;
@Autowired
private TaskRepository taskRepository;
private Member member;
private String title = "제목1";
private String description = "설명2";
private Long taskId1;
private Long taskId2;
private Long taskId3;
private Long taskId4;
@BeforeAll
void setUp() {
Setting setting = new Setting();
member = Member.builder()
.email("asdf")
.nickname("닉네임")
.password("1234")
.setting(setting)
.build();
member = memberRepository.save(member);
}
@DisplayName("일정 등록")
@Transactional
@Order(1)
@Test
void makeTask() {
make4Task();
Task task1 = taskRepository.findById(taskId1).orElseThrow(() -> new NoSuchElementException());
//title빼고 다 null일 때
assertEquals(title, task1.getTitle());
assertEquals(member.getId(), task1.getMember().getId());
assertEquals(null, task1.getDescription());
assertEquals(null, task1.getEndAt());
assertEquals(false, task1.getIsTimeInclude());
assertEquals(EIType.PENDING, task1.getEiType());
assertEquals(null, task1.getCompletedAt());
assertEquals(false, task1.getIsCompleted());
assertEquals(11L, task1.getSeqNum());
//description이 null이 아닐 때
Task task2 = taskRepository.findById(taskId2).orElseThrow(() -> new NoSuchElementException());
assertEquals(description, task2.getDescription());
assertEquals(false, task2.getIsTimeInclude());
assertEquals(11L + 11L, task2.getSeqNum());
//isTimeInclude가 false일 때
Task task3 = taskRepository.findById(taskId3).orElseThrow(() -> new NoSuchElementException());
assertEquals(0, task3.getEndAt().getHour());
assertEquals(0, task3.getEndAt().getMinute());
assertEquals(0, task3.getEndAt().getSecond());
assertEquals(0, task3.getEndAt().getNano());
assertEquals(false, task3.getIsTimeInclude());
assertEquals(11L + 11L + 11L, task3.getSeqNum());
Task task4 = taskRepository.findById(taskId4).orElseThrow(() -> new NoSuchElementException());
assertEquals(true, task4.getIsTimeInclude());
assertEquals(11L + 11L + 11L + 11L, task4.getSeqNum());
//순서 확인
List<TaskResponse> tasks = taskService.getAllTask(member).getPending().getTasks();
assertEquals (taskId1, tasks.get(0).getId());
assertEquals (taskId2, tasks.get(1).getId());
assertEquals (taskId3, tasks.get(2).getId());
assertEquals (taskId4, tasks.get(3).getId());
}
}
단위테스트의 필요성
단위테스트의 이점을 정리해보면 다음과 같다
1. 시간과 비용의 절약
- 다양한 조건에 대해 코드를 테스트하여 결함과 잠재적인 문제를 빠르게 찾아낼 수 있다
- 나중에 사소한 결함을 찾아내며 수정하는 게 오히려 단위테스트를 추가하는 과정보다 시간이 더 들 수 있다
- 단위테스트로 미리 결함을 찾아내는 게 더 비용이 적게든다
- 한 부분을 독립적으로 테스트하므로, 리팩토링해도 어느부분에 문제가 생겼는지 빠르게 파악이 가능하다
2. 품질의 향상
- 단위테스트를 이용하여 미래에 문제가 생길 수 있고, 전반적인 품질과 성능을 향상시킬 수 있는 잠재적인 오류를 찾을 수 있다
- 사소하고 찾아내기 힘든 결함들을 문제가 발생하기 전에 해결가능
- 더 높은 품질의 제품을 제공할 수 있다
- 다양한 시나리오를 실험하여 소프트웨어의 반응을 결정할 수 있다
3. 문서 제공
- 단위 테스트에는 전체 프로세스와 각 구성요소의 기능을 문서화하는 기록이 포함된다
- 이는 전체 시스템의 개요를 제공, 소프트웨어의 기능과 이상적인 사용을 보여준다
- 부적절한 사용에 대한 통찰력을 얻을 수 있음
4. 전반적인 효율성 증가
- 개별 구성요소의 효율성을 테스트할 수 있다
- 작은 구성요소가 잘 작동한다면 전체시스템의 안정성이 높아진다
- 격리된 구성요소를 테스트함으로써, 다른 구성요소에 영향을 미치기 전에 문제를 파악하고 수정할 수 있다
실무에서도 단위 테스트를 선호한다고 하며, TDD(Test-Driven Development)에서 이야기하는 테스트도 단위 테스트라고 한다. 계속 기능이 추가되고 유지보수가 필요한 프로젝트라면 단위테스트를 작성하는 게 도움이 될 것 같다.
Mock 객체를 이용한 가짜객체 구현
- 각 객체들은 서로 메시지를 주고받으면서 동작한다
- 단위테스트로 독립적인 기능을 테스트하기 위해서는 나와 메시지를 주고받는 (가짜)객체를 가정하고 기능을 테스트해야한다
- 이 때 Mock 객체를 만들어 원하는 값을 호출하도록 만든다
좋은 단위 테스트
좋은 단위 테스트를 구현하기 위해서는 다음과 같이 테스트코드를 만들면 좋다
- 1개의 테스트 함수에 대해 assert를 최소화
- 1개의 테스트 함수는 1가지 개념만을 테스트
좋고 깨끗한 테스트코드의 규칙: FIRST
- Fast: 테스트는 빠르게 동작하여 자주 돌릴 수 있어야 함
- Independent: 각각의 테스트는 독립적이며 서로 의존해서는 안됨
- Repeatable: 어느 환경에서도 동일한 결과를 반환해야함
- Self-Validating: 테스트는 성공 또는 실패로 결과를 내어 자체적으로 검증되어야한다. 사람의 개입 없이 자동으로 결과가 나와야함
- Timely: 테스트는 적시에(테스트하려는 실제 코드를 구현하기 직전에) 구현해야한다
통합테스트를 진행할 경우 데이터베이스부터 모든 것을 띄워야하므로 테스트시간이 오래걸린다. 반면 단위테스트는 각 모듈에 대해서만 테스트를 수행할 수 있기 때문에 비교적 빠른 시간 내에 테스트가 가능하다.
단위 테스트를 위한 테스트케이스를 작성하는 방법
- 유효한 응답을 확인하기 위한 테스트
- 처음에는 정상적으로 작동할 때 어떤 일이 발생해야하는지에 대한 테스트를 작성한다
- 이를 통해 기준선도 설정할 수 있다
- 잘못된 입력에 대한 테스트 응답
- 잘못된 입력에 대한 응답을 확인한다
- 유효하지 않은 데이터에 대한 응답을 위한 기준선이 만들어진다
- 여러 작업 수행
- 유효한 데이터, 잘못된 데이터를 사용하여 반복적으로 테스트한다
- 응답을 추적하여 결함을 찾는다
단위테스트의 필요성에 대해 알아밨으니 다음에는 단위테스트를 실제로 작성해보겠다!
참고자료
'소프트웨어 설계' 카테고리의 다른 글
복잡한 쿼리를 보내더라도 디비에서 처리 vs 서버에서 처리 (0) | 2024.03.14 |
---|---|
디미터 법칙(The Law of Demeter)이란? (0) | 2023.11.15 |
[책 리뷰] 객체지향의 사실과 오해 책을 읽고 정리함 (2) | 2023.10.23 |