이 시리즈는 TDD를 전혀 접해보지 않았지만, Spring boot 프로젝트에서
TDD를 하고싶은 분을 위한 class101 입니다
스프링을 배우셨거나 프로젝트를 해보셨다면 '테스팅'이라는 키워드를 들어보셨을것입니다.
테스팅이란 무엇일까요??
테스팅이란 버그를 찾는 행위입니다.
왜 버그를 찾아야할까요?? SW는 사람이 만들기 때문이고, 사람은 실수를 만들기 때문입니다.
오죽하면, "Human error는 기능이다" 라는 말까지 있을까요?
어릴 때, 종이접기만 할 때를 떠올려보면, 안으로 접어야하는데 바깥으로 접고 안으로 넣어야하는데 바깥으로 넣는 등 간단한 종이접기를 할 때도 많은 버그가 생기는데요
프로그래밍처럼 복잡한 사고의 체계를 여러 줄의 글로 기록할때 생기는 버그의 양은 말할 필요가 없겠죠?
이렇게 소프트웨어의 곳곳에 숨어있는 버그들을 찾아내는 행위가 테스팅이고
테스팅을 통해서 신뢰성있는 품질의 SW를 만들어낼 수 있는것입니다.
하지만 개발자들은 내가 생각해낸 것을 빠르게 만들어내는 것을 너무 좋아하다보니
테스팅처럼 새로운 기능을 만들어낸 것과는 동떨어진 반복작업하는 것이 너무너무 싫습니다.
하지만 자신 스스로 개발자가 아닌 엔지니어라고 생각하면 테스팅을 대하는 시선이 달라진다고 생각합니다.
내가 만든 SW는 기능이 만들어지는 것은 기본이고, 요구사항을 완벽하게 반영하며 예측할 수 있는
모든 예외에 대하여 처리할 수 있는 SW를 만든다는 자세로 임해야하기 때문입니다.
그냥 SW가 아니라 믿을 수 있는 고품질의 SW를 만들어내는 것이
사회가 소프트웨어 엔지니어에게 기대하는 바라고 생각합니다.
비단, 소프트웨어 엔지니어링뿐만 아니라 비전문가가 전문가에게 받게되는 결과물이란
당연히 그런것이어야 한다고 생각합니다.
SW가 고장을 일으키기 전에 결함 발견을 위하여
결함을 수정하고 테스트한 후 일정 품질의 수준까지 높이기 위하여
요구되는 테스트를 정해진 예산과 일정안에 효과적으로 수행하기 위하여
SW가 A라는 작동을 하기로 명세되어있는데 B로 작동한다
SW가 A라는 작동을 하지 않기로 작동되어있는데 A라는 작동을 한다
SW가 명세에 없는 작동을 한다
SW를 이해하고 사용하기 어렵고 느려서 사용이 어렵다
결론적으로, 테스팅이란 버그를 찾는 행위이고 높은 품질의 SW를 만들기 위한 좋은 도구이다! 라고 말하고 싶습니다.
다음으로는 실제로 프로젝트에서 테스팅을 해보면서 느낀 장점들에 대해 말씀드려보겠습니다
일반적으로 테스트코드를 작성한다고 하면 개발이 느려진다고 생각하기 쉽상입니다.
저도 당장 기능 만들 시간도 부족한데 테스트코드를 언제 작성해?? 라고 생각했었구요
테스트코드를 작성하는게 쉽고 빠르다는 말씀은 드리지 않겠습니다.
테스트코드를 작성할려면, 그 전에 해야할게 많거든요.
ㄱ. TDD 공부
ㄴ. 프로젝트의 언어/프레임워크에 맞는 테스트 라이브러리 공부
ㄷ. 써드파티를 포함한 테스트코드를 작성하기 위한 공부
ㄹ. 테스트코드를 잘! 작성하는 방법
ㅁ. 만들어놓은 테스트코드가 유효한지 고찰
테스트코드를 처음 작성한다면 ㄱ-ㄷ 까지는 새로 공부하게 됩니다.
물론 이 부분은 이 시리즈에서 다루게 될 것이구요.
테스트코드 작성을 한 번 배워두면 그 다음에는 ㄹ-ㅁ을 머리가 깨지게 고민하게 됩니다.
그런데, 한 번 테스트코드 작성법을 배워두면 그때부터는 개발속도가 비약적으로 증가한다고 확신합니다!
Spring boot 프로젝트를 할 때 어떻게 API가 잘 만들어졌는지 확인하시나요?
저는 테스트코드를 작성하기 전에 아래와 같은 절차로 진행했습니다.
혹시 여러분도 이렇게 하고 계시지 않으신가요?..
이렇게 했을때의 단점은 아래와 같이 너무너무 명확합니다.
1. 시간이 너무 오래걸린다(Spring boot 켜는 시간, Postman에 일일히 적는 시간)
2. 다양한 파라미터를 넣어볼 수 가 없다
- 이전에 만든 데이터를 수정하고 삭제해야 확인이 유효한 경우가 왕왕있기에
3. (2)번 때문에 숨어있는 버그가 엄청나게 많다
- 다양한 파라미터로 해보았으면 쉽게 눈치챌 수 있었을 버그조차 못 잡는다
4. 기능 수정이 힘들다
- 기능이 수정되면 새로 확인을 해보아야하는데, 확인절차가 번잡하고 fragile하기에
5. 써드파티 테스트 및 관리가 어렵다
- MySql 안 키고 시작해서 실패하신적 많으시죠?..
또, API가 Redis 를 사용하는 경우, 시험후에 Redis또한 정리해주어야죠?
그리고 Redis 같은 써드파티는 Spring boot 실행시에 Bean을 생성해주어야하기에
실행전에 Redis가 떠있어야 합니다.
Elasticsearch나 Kafka를 사용하는 경우도 동일합니다..
6. 멋지지가 않다
- Postman 으로 딸깍딸깍 하다보면 조금 멋스럽지 않은 개인적인 느낌..이지 않나요?
그런데! 놀랍게도 위의 문제를 모두 해결하는 방법이 있다?

바로 테스팅입니다!
앞서 말한 단점들을 어떻게 해결하는지 나열해보겠습니다

이제 테스트코드를 작성해볼 모티베이션이 충분할까요?
이 시리즈는 Spring boot JUnit 이 아니라 test와 TDD 자체를 다루기에
소프트웨어 공학수준에서의 테스트에 대해서도 다룰거에요
앞에서 "통합테스팅" 이란 키워드가 나왔었는데요.
테스트의 종류에 대해 이야기 해보겠습니다.
테스트는 "유닛 테스트, 통합 테스트, 인수 테스트"로 나뉩니다.
가장 좁은 범위의 테스트이자, 가장 기초적이며 중요한 테스트입니다.
각 각의 유닛이나 컴포넌트들이 의도한대로 작동하는지 확인해보는 목적의 테스트입니다.
여기서 테스트 대상은 클래스가 되거나 메소드가 되는것이 일반적입니다.
Unit test는 주로 "WhiteBox test"를 통해 수행됩니다.
WhiteBox test란 BlackBox 의 반대말처럼, 안이 훤히 들여다보이는 상태로 테스트하는 것입니다.
말인즉슨, 테스트하고자하는 컴포넌트 내부의 로직과 의존성을 모두 아는 상태로 테스트하는것이죠
그렇기 때문에, 아래와 같은것이 유닛 테스팅의 주 관심사가됩니다.
그렇기때문에 코드가 변경되면 테스트가 실패하게 됩니다(주로 테스트가 깨진다고 합니다)
따라서, Whitebox 테스트는 코드변경시에 같이 수정해주어야하는 필요성이 있습니다.
너무 fragile 한게 아닌가? 생각할 수 있지만, 커밋시에 CI/CD 를 사용하여서 항상 테스트를
수행할 수 있게 한다면, 협업 과정에서 정말 큰 도움이 됩니다.
물론, 이 Whitebox 테스트는 코드 내부에 종속되기 때문에 컴포넌트가 지나치게 커지면
테스트하기 어려운 문제가 있습니다. 그런데 한 편으로는 이렇게 생각할수도 있는데요.
TDD에서는 테스트하기가 어렵다면 설계 개선이 필요하다는 신호 라고 보는데
하나의 컴포넌트가 지나치게 많은 일을 한다고 볼 수 있기 때문에, 이런 경우에 가능하다면
컴포넌트의 책임을 분리하여 좋은 구조로 나아갈 수 있는 계기가 될 수도 있겠습니다.
너무 지겨운 얘기만 했으니, 우리의 관심사인 Spring boot 관점에서 얘기해볼까요?
Unit Test는 주로 Service, Controller와 같은 Spring Component 부터
우리가 직접 만들어준 Bean이 아닌 클래스들에 적용하여 사용하게 됩니다.
가장 시간을 많이 절약해주는 일등공신이라고 생각합니다.
Service 를 테스트하는 간단한 예시코드를 볼께요

새로 보는 키워드가 너무 많아 헷갈리시지만, 핵심적인 부분만 보자면
메소드 이름으로 보건데 submitNewOrder 라는 기능을 테스트하려는 것 같죠?
주목하실 부분은 109라인인데요, storeService는

이런 인터페이스인데요, 테스트코드에서 storeService의 submitNewOrder라는
메소드를 호출해서 결과값을 받아낸것을 볼 수 있어요.
참고! 여기서 사용된 storeService는 StoreService를 구현한 StoreServiceImpl이란 클래스에요
이 StoreService의 코드를 바꾸고 정상적으로 동작하는지 확인하고자 하면
다시 PASS_submitNewOrder 라는 테스트 메소드를 실행시켜보면 끝이에요.
Service를 불러주는 Controller에 API를 쏘기 위해 Spring boot를 시작하고
Postman을 켜서 요청을 보낼 필요가 전혀 없죠.
진짜 테스트하고자 하는것만 할 수 있게 해주는 것이 테스팅이에요
그런데 여기서 의문이 있으시겠죠! Service는 주로 Repository를 의존하거나
Redis, Kafka 같은 써드파티를 사용하는 경우가 많지않나요? 라구요
Unit Test의 핵심은, 테스트하고자 하는 컴포넌트의 기능자체만 테스트하는 것입니다.
즉, Service가 의존하는 클래스나 써드파티는 제외되어야 하는것이죠.
그런데 이것을 제외하고는 기능이 동작하지 않는것은 불보듯 뻔하잖아요?
그래서 Mocking 이라는 개념이 Unit Test에서 핵심적인 요소입니다.
Mock은 모조품이라는 뜻이죠? 이름에서 알 수 있듯이, 테스트하고자 하는 컴포넌트 외에는
모두 모조품으로 바꿔치워서 얘네들이 있는것처럼 하자는것입니다.
위의 테스트코드에서 given, when이 바로 mocking 을 해주는 과정입니다.
이것은 이 후 포스팅에서 실제로 사용하게 될거에요(반드시)
Unit Test를 보았다면 Integration Test는 간단합니다.
Unit Test에서 Mocking으로 바꿔서 의존하던 대상들을 실제로 바꾸어서 테스트하는겁니다.
Unit Test가 이루어진 후에 Integration Test가 이뤄지는것이 일반적인 방식입니다.
실제 객체들로 바꾸었을 때, 시스템이 정상적으로 통합되어 작동되는지 확인하기 위한 테스팅입니다.
너무 뜬구름 잡듯이 말해서 혼동스러우실텐데요. 예를들어보겠습니다.
Unit Test의 경우
Service에서 Repository를 사용합니다. Unit Test에서 Repository는 모조품이기에
어떠한 DB를 실제로 작동시키지 않습니다. 단지 어떤 질문에 대해(id 가 1인 유저를 줘)
정해져 있는 답(유저A 반환)을 할 뿐, 실제 DB에 저장되어 있는 값을 주지 않습니다.
Integration Test의 경우
Service에서 Repository를 사용합니다. 여기서의 Repository는 실제 DB와 연결된
Repository입니다. 이 경우에 실제 MySQL, Oracle, PostgreSQL로 각 각 바꿔가면서
실제로 시스템이 동작하는지 테스팅합니다.
시스템에 변화가 생겼을 때(새로운 패치, 업그레이드, 버그수정)시에 기존에 잘 작동함을 확인한
테스트들이 시스템 통합후에도 여전히 깨지지 않았는지 확인하는 테스트입니다.
Integration test와 유사하지만, 컴포넌트에 중점을 두기보다는, 실제 사용하는데 초점을 둡니다.
그래서 실제 고객이나 유저가 SW가 요구사항대로 작동하는지 확인해보는 과정입니다.
가장 마지막 단계의 테스팅입니다.

TDD라는 말은 워낙 추상적인 단어이니만큼 많이 혼동되고 각기 다른 정의대로 사용하는 것 같습니다.
사실 이 시리즈도 TDD이지만, 실제 TDD 그 자체를 완벽히 따르기는 쉽지 않습니다.
TDD는 SW를 만드는 것이 테스트 작성에 의해 가이드되는 방식입니다.
Kent Beck이 고안한 단어이고, 아래와 같은 절차를 반복적으로 따르는 개발방식입니다.
(1)을 읽고서 뭔가 이상하다고 생각하지 않으셨나요? 코드가 없는데 테스트를 작성합니다

믿기지 않지만 아직 코드가 없지만 테스트를 작성하고, 테스트가 실패하는 것을 손수 본 후에
(2)에서 만들려는 코드를 작성하게 됩니다..!
물론, 이러는데는 이유가 있습니다.
1. 순서를 반대로 하면 테스트하기 쉬운 코드를 만들도록 유도된다
2. 테스트 자체가 설계활동이 된다
3. 모듈화된 코드가 만들어진다
즉, 테스트코드는 코드를 작성하고 그것을 테스트 하기 위해 작성한다는 통념에서 벗어나
테스트코드 먼저 작성하고 실제 코드를 만든다는거죠.
놀라운 아이디어입니다..
여기까지 아주 기본적인 테스팅에 대한 설명이 끝났습니다.
개인적으로 아래와 같이 진행해보고 싶습니다
1.(본 포스팅) 테스팅이란?
2. JUnit, hamcrest
3. Spring boot test
4. Mockito, PowerMock
5. 좋은 테스트 작성하기