신입이 느낀 E2E 테스트

noname2048·2023년 6월 21일
1
post-thumbnail

직장에서 있었던 일을 회고하며 정리한 글입니다.
부족한 면이 있다면 언제든 댓글로 남겨주세요 !! 🙋

👉 동기부여

직장에서 장고(Django)를 이용해 제품(Product)을 개발했습니다. 저의 사수는 계층 분리에 대해 고민하고 적용하는 분이라, 장고에서도 계층 분리 적용하여 제품을 경험할 수 있었습니다. 팀이 UnitTest를 선호하는 것과 달리 개발과정에서 저는 E2E 테스트를 더 좋아했는데, 그 이유를 설명하고자 합니다.

✅ 클린아키텍트

계층 분리를 위해 템플릿에서 제공되는 파일구조를 수정하여 서비스 계층을 추가하였습니다. 그리고 최대한 서비스 계층에 비지니스 로직을 모아 코드의 집중도를 높이고자 했습니다. 이를 위해 자주 쓰이는 DRF serializer 와 Django Model에 로직을 사용하는 것을 최대한 자제하기로 약속했습니다.

배터리 팩처럼, 대부분을 컨벤션으로 기반으로 구성되는 장고는 다른 프레임웤에 비해 테스트를 쉽고 빠르게 구성할 수 있었습니다. 시간이 급박한 스타트업 특성상 서비스에 작성된 함수를 기반으로 유닛테스트를 작성하길 원했고, 서비스를 기반으로 테스트 할 것을 논의했습니다.

다만 저는 UnitTest와 E2E테스트에 대한 개념이 조금 부족했고, 필요한 방식으로 테스트를 작성했습니다. 처음에는 유닛테스트인줄 착각했었는데 제가 작성하는 테스트는 E2E 테스트라는 것을 알게되었습니다.

📝 계층을 분리한 방법

입사할 때, 클린아키텍처에 대해 잘 알고 있지 못했습니다. 제 사수는 JAVA 진영에서 클린아키텍처와 헥사고날 아키텍처를 공부하여 이를 Django 진영에 적용할 방법을 찾고 있던 멋진 사람이였고, 사용하고 있던 Django 의 계층은 다음과 같이 구성하였습니다.

Django
config/urls.py -> app/urls.py -> app/apis/api.py -> app/services/services.py -> app/models/model.py

E2E 테스트가 필요했던 이유

여기서의 E2E 테스는 벡엔드에 한정지어 설명하겠습니다.

🤔 serializer가 받아들이는 옵션에 대한 불명확함

REST API를 좀더 빠르게 작성하기 위해 사용하는 DRF(django-rest-framework) 안에는 입출력단을 검증하는 Serializer라는 클래스를 사용합니다. Serialier 기준으로 필드값안에 있는 필드가 어떻게 들어와야 하는지 정의해줄 수 있는데, 이게 머리속에 잘 정리되지 않아 매번 헷갈렸습니다.

만약 name과 memo 라는 파라메터를 지정한다면 다음과 같은 옵션을 지정할 수 있습니다. (memo를 기준으로 설명합니다.)
1. {name: "velog"}
2. {name: "velog", memo: null}
3. {name: "velog", memo: ""}
4. {name: "velog", memo: "1*&(!@&(#"}
5. {name: "velog", memo: "정상적인 메모"}

위와 같은 많은 상황에서 3번과 5번만이 정상적인 데이터로 지정할건지, 1, 2 번 역시 정상적인 API 응답으로 처리할 것인지에 대해 명확하게 정의하지 않았었습니다.

🙂 SerializerMethodField 검증

tag field 와 같은 경우에 다음과 같이 string으로 전달 받았습니다. "python,장고,Django" 이를 검증하려면 CharField 타입으로 정의하고, validation을 붙여주어야 하는데, 이를 위해 SerializerMethodField 를 추가 하여 사용했습니다. 이러한 필드 타입을 내부로 가져올때는 리스트로 변환하기에 Request를 잘 받아 내부 인자로 넘겨주는지, 잘못된 Request에 대해 적절한 Error 메세지를 리턴하는지 확인이 필요했습니다.

🙋 drf_spectaculur 에 대한 검증

괜찮은 벡엔드라면, FE와 협력하기 위해서 Swagger를 작성합니다. 팀은 drf_spectacular 라는 라이브러리를 통해 FE에게 swagger 문서를 전달하려고 노력했습니다. drf_spectacular는 훌륭하게 만들어줄 뿐만아니라, Python의 컨벤션인 snake_case를 JS의 컨벤션인 camelCase 로 바꾸어 내보내고, 반대의 경우도 수행할 수 있도록 세팅할 수 있었습니다. 다만, 세팅을 잘못해서 이 기능이 적용되지 않고 있었다는 문제를 발견하고 나서는 이 기능이 잘 작동하고 있는지 필요하다고 생각되었습니다.

👌 적절한 타협점

위와 같은 의사결정을 해서 E2E 코드를 작성하여 코드를 작성하여 사수와 함께 논의하였습니다. 그리고 고민끝에 다음과 같이 test 를 나누어 작성하기로 하였습니다. app/tests/apis/endpoint.py, app/test/services/feature.py.

API 테스트와 유닛테스트를 파일구조에서 분리했고, 이 해결책은 지금 돌아보아보아도 적절한 해결책이였다고 생각합니다. 나는 처음쓰는 타입의 필드나 serializer의 특수한 기능을 이용하여 입출력을 검증해야 할때는 apis 폴더 밑에 작성했고, 간단한 로직의 경우 serivces 밑에 테스트를 구현하는 것으로 TDD를 끝냈다. 가끔 API 구현이 아주 바쁠때에도 apis에 테스트를 작성하여 검증했습니다.

다만, 위에 적힌 문제들은 입출력이 정확하다면, 따로 테스트할 필요는 없었기에 service를 위주로 테스트를 작성할 때도 많았습니다. api와 service 모두 테스가 필요했으므로, 한쪽만 고집하지 않은 일도 잘했다고 생각합니다.

👀 여러가지 범주의 테스트

회사가 전에 개발하던 제품에서는 FE를 포함해 UI까지 시뮬레이션 하는 전체 E2E 테스트가 존재하였습니다. 제가 개발하던 테스트는 이러한 E2E를 포함하지 않았기에 유닛테스트라고 초기에 헷갈렸었는데, 지금은 여러가지 범주의 테스트가 존재한다는 것을 알았습니다. API 테스트라고 말하기도 하는 테스트는 벡엔드 입장에서 E2E 테스트라고도 불리는 것 같습니다. 앞으로는 명칭 자체보다 어떤 기능을 위해 테스트를 만드는지 확인하려 합니다.

다양한 범주의 테스트가 있기 때문에 헷갈리지 않으려면, 아키텍트나 개념이 명확하게 구분되어 이를 분리하고 테스트 할 수 있으면 정말 좋겠습니다. 하지만 경험상 서비스를 개발하다 보면 지금처럼 분리하기 어려운 회색지대를 보게 됩니다. 다행히 클린아키텍처를 이해하고 있는 사수에 의해서 빠르게 해결되었지만, 스스로 해결할 수 밖에 없는 상황이 닥쳤을때 잘 해결할 수 있을지는 조금 걱정됩니다. 필요에 따라 잘 분리하고 사용할 수 있도록 열심히 경험을 키워야 할 것 같습니다.

나름대로 조금 더 세분화 해보면 다음과 같이 정리해볼 수 있을 것 같습니다.

  1. tests/unit/feature.py - 유닛테스트: 아주 작은 기능을 테스트한다. (모델이나, 도메인내 서비스에서 자주쓰이는 중요한 로직을 테스트 할 수 있을것 같다.)
  2. tests/serivces/service.py - 서비스테스트: 작은 서비스를 테스트한다. (CRUD가 포함된다.)
  3. tests/apis/api.py - api 테스트(e2e테스트): api를 가지고 입출력까지 모두 포함하여 테스트한다. (입출력에 대한 검증 -> 입출부가 신뢰성이 높아지면 없애는게 맞을 것 같지만, 남겨보려한다.)
  4. tests/integration/integration.py - 통합테스트: 특정 시나리오에 따라, 슬랙메세지 발송, 이메일 발송 실패 로직 등을 테스트한다.

요약

실제로 이렇게 작성된 테스트를 기반으로, 개발중에 발생할 수 있었던 치명적 오류를 여러번 막을 수 있었습니다. 물론 시간이 금인 스타트업에서는 개발이 더욱 중요하다는 사실을 잊어서는 안되지만, 가끔은 이렇게 테스트를 작성하며 다른 사람이 코드를 수정할 때 오류를 막아주길 기도하는 일이 종종 있습니다.

다만, 시간이 부족한 만큼 무분별한 테스트를 지양하고, 적절하게 사용할 수 있다면 개발팀에 마음의 안정이 찾아왔던 경험을 적어보았습니다.

번외

테스트를 위한 데이터 세팅의 고통

테스트를 하기 위해서는 실제와 같은 데이터가 필요하고, 이는 유저 시나리오나 실제 데이터 일부를 참조하여 만들게 됩니다. 서비스가 개발되면 개발될 수록 복잡한 DB를 설계해야 했고, 이는 개발하는 것 만큼이나 픽스처를 생성하는데 어려움을 느꼈습니다.

처음에는 유저와 게시글만 만들면 되었지만, 유저와 유저 팔로우와 게시글과 덧글과 덧글의 덧글과 덧글의 덧글의 카테고리와 .. 등등 이였습니다.

테스트를 구체하면 구체화할 수록 DB가 커져가고, 중복되는 부분이 많았기에 일부를 픽스처를 이용해 고정하려고 했으나, 테스트하려는 기능과 필드마다 필요한 픽스처가 달랐습니다. 이에 공통된 픽스처를 만들기는 거의 불가능했습니다. QA(Staging)를 위한 픽스처 구성은 가능했지만, 유닛테스트의 테스트 케이스마다는 로우코드를 사용하여 테스트를 작성했습니다.

심지어 나중에는 통계를 위해서 생성시각, 수정시각, 다른 데이터들과의 연결성도 고려해야 했기에 해당 부분의 적절한 테스트를 작성하기에 시간이 너무 많이 들어가, 잘 테스트되었다고 말하는게 거의 불가능에 가까웠습니다. 그저 함수가 잘 작동하나 핵심기능만을 골라 테스트하는게 전부였는데, 복잡성이 커지고 어려울수록 의도했던 기능을 테스트로 작성하기가 너무 까다로웠습니다.

jest의 장점을 pytest 로 가져올 수 없을까

NestJS가 궁금하여 Nest를 공부하던 중 Jest를 만나게 되었습니다. Django의 UnitTest 보다 UI가 깔끔하고 직관적이였습니다. 마음에 들었던 점이 몇가지 있어 개인적으로 python의 test라이브러리를 구축하는 팀들이 이러한 장점을 가져와 주었으면 좋겠다고 생각했습니다. 제가 생각한 장점은 아래와 같습니다.

  • test_로 함수 네이밍을 강제하는 장고와 달리 spec.ts 등으로 확장자를 통해 test file을 결정할 수 있음
  • todo.it 과 같이 테스트를 작성해야 함을 기록할 수도 있음
  • describe 안에 describe 등을 두어 트리구조처럼 중첩시키는 것
  • watch를 통해 테스트 파일을 hot reload 로 바뀐 부분을 바로바로 테스트해주는 것

django의 test 구성은 fastapi 보다 간편하다

시간이 충분하다면 기능을 구성할 수 있겠지만, 빠르게 테스트 환경을 구축할 수 있다는 점에서 유리한 점이 있습니다. 다른 프레임워크는 DB에 대해 transaction을 롤백하거나 TestDB를 생성하고 유지하는 기능에 보다 많이 손이 가게 되는 반면, Django는 오랫동안 유지보수 되어온 만큼 다른 프레임워크를 test에 굉장한 장점을 보입니다. 아래와 같은 Test시의 이점은 Django를 선택하게 하는 다른 장점이 되는 것 같습니다.

  • 설정만하면 test db를 자동으로 생성하고 해체해주는 점.
  • fixture를 내보내기 하거나 가져오기 해서 설정 할 수 있다는 점.

test case 를 작성하며 조심해야 할 내용

  • test case를 잘못 작성했는데 통과해서 통과되는 줄 잘못알아 문제가 커지는 경우
  • 새로 변경된 spec에 따라 testcase를 구성하는 경우, 관련된 모든 testcase를 수정해야 하는 경우가 있을 수 있음
    • 이 경우 문제가 생긴 test case를 수정하는 것보다 죄다 지우는게 실질적으로 좋은 해답일 수 있었다.
  • datetime 과 같은 모듈을 mock 하기는 매우 힘들다. 따라서, 복잡한 함수를 만들 수록 인자등으로 분리하는게 더 편하다. (이동욱 개발자님의 제어할 수 없는 것에 의존하기 않기)
profile
설명을 쉽게 잘하는 개발자를 꿈꾸는 웹 개발 주니어

0개의 댓글