Test를 대하는 자세
- 이창용(카카오페이)
- 14년 ~ SI, NHN Ticketlink, 네이버지도, 카카오페이 보험, 송금
- 자바 스프링, JPA, 코틀린 등 ...
원시적인 테스트
- 작성한 코드가 올바르게 실행되는지 확인하는 작업
- 기초적인 방식
- 로컬 서버를 구동하여 직접 API를 호출
- Postman, InteliJ의 HTTP 클라이언트 등 ...
- 개발 서버에 배포하여 화면을 통해 API 결과 확인
- 인터셉터 등의 인증 절차에 걸려서 결국 포기하고, 배포를 선택하기도
- 외부 라이브러리 등을 사용할 때는 호출 결과를 표준출력을 통해 확인
subString
등 사용할 때 어디까지 잘라야 하는지, 인덱스가 0부터 시작하는지 등 ...
- Jackson 등으로 직렬화한 결과를 확인하고 싶을 때
- 이런 방식은 상당히 귀찮고 불편, 어려움을 만났다고 거기 멈춰서는 안 된다.
- 이런 귀찮은 방식에 길들어 관성이 생기지 않도록 하는 것이 좋은 개발자가 되는 길이다.
코드로 하는 테스트
- 작성한 비즈니스 로직 코드를 다른 코드로 확인하는 작업
- 개발자 레벨에서 파라미터를 조작하여 경계값에서 기대한 대로 동작 하는지 확인할 수 있음
- 비즈니스 코드 외에 추가로 코드를 작성해야 하며, 때때로 비즈니스 코드를 작성하는 것보다 더 많은 시간을 요구함
- 테스트 코드 역시 코드이기에 지속적인 관리가 필요함
- 좋은 개발자는 자신이 작성한 코드를 지속적으로 관리한다. 테스트 코드 또한 이런 관리 대상에 포함된다.
- 테스트 코드도 클래스 분리를 해야하며
- 중복 코드를 발생시키지 않으면서 여러 사례를 커버할 수 있어야 함.
- 마틴 파울러가 리팩토링에서 말했듯이 3회 이상 반복된다면 리팩토링이 필요하다.
자바의 경우
- JUnit: 자바 진영의 훌륭한 테스트 프레임워크
- 켄트 백, 에릭 감마
- 테스트 코드를 작성하기 쉽게 하는 mockito와 같은 테스트 보조 라이브러리들도 존재
- 다양한 assertion 라이브버리 등 테스트 코드만을 위한 프레임워크, 라이브러리가 다양
- 예전에는 hamcrest, 현재는 assertj가 널리 쓰임
- 스프링 프레임워크에서도 위와 같은 테스트 생태계를 통합, spring-test라는 테스트 전용 모듈을 제공
- 스프링 자체가 프레임워크의 프레임워크 수준으로 크고 추상화된 통합 프로젝트
- 기본 프로젝트 구조에도 test만을 위한 디렉토리가 존재
- 프로덕션 코드 말고 테스트 코드까지 배포하면 안 되니까
왜 또 코드를 작성하는가
- QA가 있는데도 왜 작성할까?
- 전문 QA 조직이 있는 회사는 생각보다 많지 않음
- QA는 단순히 내 코드를 확인해주는 사람들이 아님
- QA는 내 코드 라인 한 줄, 한 줄을 테스트해주지는 못함
- 작은 변경 하나하나를 요청할 수는 없음
- 개발 완료라고 하고 시작부터 에러를 내뿜는 API는 현업에서도 자주 보게 된다.
- 코드로 남기면 히스토리 축적이 된다.
- 사람이 시나리오를 통해 진행하는 테스트는 그 결과가 축적되지 않음
- 실제로 프로그램 개발이라는 작업은 항상 신규기능만 개발하는 것은 아님
- 새 기능을 개발 하다가 수정한 코드로 인해 기존 기능에서 예상치 못한 에러를 만나는 것은 흔한 일임
- 코드는 작성 이후 그 자체로 파일이 되어 남기에 테스트 결과가 지속적으로 축적됨
- 새 기능을 개발할 때도 기존 기능에 대한 테스트 코드가 남아 있고, 이를 지속적으로 실행하기에(CI), 기존 기능에 side effect가 없음을 보장받을 수 있다.
- 지속적으로 프로덕션 코드에 대한 피드백을 받기에 안전하게 리팩토링, 기반 기술 버전업(프레임워크) 진행 가능
- 실제로 regression test가 제대로 진행되지 못하는 환경에서 프레임워크의 버전을 올리지 못하고 처음 프로젝트 세팅 때 버전으로 수 년간 서비스를 진행하는 곳이 맞음
- > 프로젝트는 지속적으로 레거시화된다.
- 개발자는 이런 코드를 욕하는 입장이 아니라, 개선하는 입장이다!
- 단순 로직 테스트는 개발자 선에서 모두 진행한 후 다음 단계로 넘어가기 때문에 QA 조직과 신뢰도가 형성되며, QA 조직은 좀 더 고도화된 프로덕트 테스트에 리소스를 투입할 수 있음
테스트를 작성할 때 장벽들
- 테스트코드 자체의 생산성 논쟁: 테스트 코드를 작성하면 개발 시간이 오래 걸린다.
- 참고: 소프트웨어 장인(http://www.yes24.com/Product/Goods/20461940)
- 테스트 코드를 작성한다고 하면 할 일 없냐는 소리를 듣는다.
- 이런 말을 하는 시니어도 문제지만, 개발과 테스트를 분리한 당사자도 문제다.
- 외부 직군은 개발 기간을 독촉하는 것이 정상, 그 와중에 품질을 관리하고 거기에 맞는 선택을 하는 것은 개발자의 역할
- 비즈니스 로직이 담기는 Service를 테스트하기가 너무 어렵다.
- 테스트를 못 할 정도로 의존성이 복잡하게 엉켜 있다면 이건 그 서비스 자체가 분리가 필요하다는 의미로 생각하자.
- 의존성이 한 5개씩 있다면...
private method 테스트 해결하려 접근해봤던 방법들1. private 메소드를 사용하는(포함하는) public method를 테스트한다 2. 리플렉션으로 사용해서(접근해서) 푼다 3. private를 default로 변경
- public 메서드 호출을 이용하여 테스트
- 바람직한 방식
- 하지만 너무 거대한 private 메서드를 (게다가 여러 번) 호출한다면?
- 어쩌면 저렇게 거대한 private 메서드의 존재 자체가 잘못된 것은 아닐까?
- 별도의 클래스로 분리해야 하지 않을까?
- 하나의 하위 컴포넌트를 만들고, 테스트용 클래스에 주입
- default 접근자로 변경하여 사용하기
- 테스트 때문에 프로덕션 코드의 접근 제어자를 건드리는 것이 과연 적합한가?
- 리플렉션 사용하기
- 타입 안정성 때문에 부적합
Mocking
- 테스트 코드를 작성하다보면 이런저런 가정을 세운 후 작성을 할 때가 많음
- 이런저런 가정들을 실제 코드로 표현해주는 것이 Mock 라이브러리
- 처음 사용할 때는 편리하고 자신감을 심어주지만 테스트가 Mocking 코드로 뒤덮히면서 실제 테스트 목적을 찾기가 어려워진다.
- 너무 타이트한 가정으로 인해 실제 프로덕션 코드가 조금만 변경되도 테스트가 실패하게 되고, 그 실패하는 이유를 찾기도 어려워져서 나중에는 테스트 코드 자체를 돌리지 않는 사태가 발생
- Mock 라이브러리 사용을 최소화하려면 인터페이스와 DI를 적극적으로 사용해야 함
- 내가 생각하기에, mocking은 가급적 자제하고 인터페이스를 지향함이 낫다.
- 근본적으로는 테스트 코드 가독성을 위함임
- 정확히는 mock 라이브러리를 이용하지 않는다는 의미!
- 프레임워크의 도움 없이도 DI를 이용해서 stub 구현체를 보낸다는의미
- 과거에는 필드 주입을 많이 사용해서 이것이 힘들었음, 요즘에는 생성자 주입이 대세라 이렇게 가기 좋다.
TDD
- 테스트 코드를 작성하는 수준을 넘어서 테스트 코드를 먼저 작성하는 것
- 어떻게 생각해보면 말도 안 되는 방식
- 실패하는 테스트를 작성하고, 테스트를 성공한 후 코드를 리팩토링하는 사이클을 반복
- 프로덕션 코드는 테스트를 통과할 정도로만 작성해야 함
- 추가적인 가정들이 들어간다면 그것들을 테스트로 표현해야함
- PhoneNumber 클래스, 송금 유효성 체크 클래스를 테스트 코드 작성하면서 구현
라이브 코딩
- 테스트 메서드명은 한글로 사용
- 프로덕션도 아니고
- 어차피 잘 읽을 수 있어야 하니까
- JUnit5의 assertEquals -> 순서가 직관적이지 않음: 예상 값(expected)이 먼저라서..
- Getter 없는 철저한 캡슐화
- Getter 사용은 낮은 수준의 캡슐화임
- 굳이 PhoneNumber 클래스를 만드는 이유: 불변식 통한 검증을 위하여
- 별개로, 이 PhoneNumber 객체는 VO임
Q & A관련 정리
- 테스트 커버리지 100퍼센트: https://toss.im/slash-21/sessions/1-6
- 100퍼센트 커버리지 때문에 생기는 트레이드 오프가 과연 적절한 것인지는 고민해봐야 할 것
- 내 경우는 컨트롤러 테스트는 통합 테스트이다.
- 인터셉터, ArgumentResolver 싹 다 통과한 그런 경우 말하는 것임
- 어떤 클래스/계층부터 개발하면 좋을지, 스타일
- Controller, Repo, Entity들을 우선 작성한다: 순서를 따지자면 손 가는 대로,
- 주요 로직은 도메인 객체에 넣는다.
- 서비스는 그저 호출만 한다. 서비스에 날 것 그대로의 연산은 들어가지 않음
- DDD
@Service
,@Repository
애노테이션 내에는 DDD에서 말하는 개념이라고 정의되어 있음
- 스프링의 위와 같은 Component 역할 애노테이션들은 DDD 기반으로 이해해야 한다!
- stub, mock, spy?
- 다 사실은 mock이다.
- 구별하면 좋지만, 실무에서는 다 mock 처리
- 실무에서는 테스트 자체를 안 짜는 사람도 많다.
- API 개발 시에 모두 엄격하게 명사로 url을 처리한다면 제약 사항이 많아져서, 내 경우에는
cancel
등을 url에 포함하기도 한다.
면접
- 주니어가 합격률이 오히려 높다.
- 주니어는 몇 개를 몰라도 잘 한다고 하지만, 시니어는 조금 몰라도 엄격하게 보게 된다.
- 신입 개발자의 경우 자바 스프링을 너무 열심히 공부해오는 경우가 있다.
- 회사, 면접관 따라 다르긴 하지만 개인적인 생각으로는
- 일단 우리 회사는 신입에게 자바 스프링에 대한 이해도를 크게 기대하지 않는다.
- 어떻게, 무엇을 얼마나 열심히 공부했느냐가 좀 더 중요 포인트였다.
- 탈락자의 경우
- 자료구조를 몰라서 떨어지는 경우가 있음
- Array, Queue, Stack, ... 등 간단한 것들
- 신입 기술 면접은 면접관도 공부해 와야 한다.
- 면접 시 항상 하는 질문? -> 없음
- 경력 기술서 / 자기소개서(신입)을 보고 질문 사항을 추려서 간다.
- 항상 하는 질문이라고 한다면, 궁금한 것 있는지?