1주차 워크북을 보았다. 워크북에서는 해당 주차의 목표가 무엇인지 스스로 점검하고, 해당 주차의 목표를 지키기 위해서 보면 좋을 learning material들을 추천해 주고 있었다. 

 

해당 learning material 중에서 Contributing to Django라는 문서가 있길래 클릭해 보았다. 해당 문서에서는 django에 어떻게 기여할 수 있을지를 알려주고 있었다. 그리고 나는 코드 패치를 쓰는 것만이 장고에 기여하는 것이라고 생각했는데, 그 외에도 문서화를 하는 등의 다양한 기여 방법이 있었다. 

 

나의 우선적인 목표는 코드로 기여를 하는 것이었기에, 관련된 문서를 눌러서 해당 프로그램과 관련된 튜토리얼을 진행하였다. 

 

중간에 'pip install pylibmc' 부분에서 에러가 났다. GPT 찬스를 써서 'brew install libmemcached' 명령어를 실행해도 해결되지 않았는데, 다행히 중간에 발견한 어떤 글을 보고 따라해 보았다. 그러다가도 에러가 계속되었는데, 원인상 'brew install libmemcached' 명령어로 설치한 libmemcached 패키지의 경로를 찾을 수 없어서, 즉 해당 패키지가 지정된 경로에 있지 않거나 설치되어 있지 않아서 발생하는 에러 같았다. 

 

여러 블로그들을 탐방한 결과 django open forum에도 해당 이슈가 올라와 있었다. 해당 글을 읽고 커맨드를 따라해 보면서 오류를 해결할 수 있었다. 

LIBMEMCACHED=/opt/homebrew pip install pylibmc

 

테스트에 필요한 패키지들을 설치하는 것 까지는 완료되었다. 그런데 다음 명령어를 실행하니 아래와 같은 오류가 발생하는 게 아닌가. 

python3 runtests.py

 

이런 에러가 왜 나지 싶어서 소스 코드를 보았더니 의심가는 지점이 있었다. 실행한 runtests.py 파일의 46-48번째 라인이다. 일부러 이런 오류가 나도록 설정해 놓은 걸까? 

 

추측해 보기로는 Django 6.0 버전 전까지 나는 에러 같기도 하다. DeprecationWarning은 이제 곧 제거될 예정인 기능을 사용할 때 나는 경고인 것으로 안다. 그런데 그렇다면 RemovedInDjango60Warning은 무엇일까? 60이 6.0을 말하는 것인지, 60번째 버전을 나타내는 것인지 잘 모르겠어서 모호했다. 그런데 6.0 버전은 한참 남았으니 60번째 버전이라는 해석도 가능할 것 같다. 

 

어쨌든 위의 에러가 왜 나는지는 다시 생각해 보니 알 것 같았다. 현재 python 3.12(내가 사용하는 버전)에는 RemovedInDjango50Warning은 있지만 RemovedInDjango60Warning은 없어서 나는 문제였다. 왜냐하면 지금 실행시킨 코드는 정식 출시된 버전이 아니라 개발 버전의 코드이기 때문에 그런 것일 수도 있겠다. 

 

그렇다면 어떻게 해야 할까? 일단 이 에러를 무시하는 것이 맞을까? 아니면 파이썬의 최신 버전(3.13)을 설치하면 에러가 해결되려는지는 잘 모르겠다. 우선은 보류해 보자.


Djangonaut의 첫 weekly meeting을 마쳤다! 

영어 회의는 처음이라 자기소개할 때 진땀을 뺐지만 영어를 잘하지 못합니다... sorry! 하고 잘 넘어갔다. 회의의 분위기는 kind, charming, welcoming...했다. 뉴비를 환영하는 분위기였다. 

 

질문 시간에는 '만약 티켓을 할당받았는데 그 티켓에 대한 배경지식이 없어서 잘 처리하지 못하면 어떻게 하죠' 라고 질문했는데, 매우 자연스러운 일이며 그럴 땐 도움을 요청하면 된다고 했다. 공식적으로는 django forum에 discussion을 올리고, 만약 공식적으로 밝히기 좀 망설여진다면 디스코드에 밝혀도 된다고 했다. 

 

그리고 navigator와 captain의 역할이 나눠져 있는 점도 신기하면서 좋았다. navigator는 technical helper, supporter 같은 역할을 하고, captain은 emotional, mental helper, cheerleader 같은 역할을 한다고 하셨다. 

 

그리고 티켓에 대해서 공식적인 deadline은 없다고 한다. 왜냐면 티켓별로 그 범위가 천차만별이므로... '기간 내에 못 끝내면 어떡하지?' 라는 걱정은 안 해도 된다고 해주셨다. 

 

미팅은 다음 주부터 항상 같은 시간에 매주 진행하고, captain 분과는 격주로 1:1 미팅을 진행한다. 새삼 djangonaut들에게 신경을 많이 써주시는 것 같아서 마음이 따듯해졌고 훈훈한 시간이면서도, 이 활동을 잘 경험하고 기록해서 앞으로도 이어갈 수 있게끔 하고 싶었다. 

 

 오늘 배운 것

이전에 작업하다가 미뤄두었던 RN-Django e2e test 이슈를 다시 가져와보았다. 매번 앱을 켜고 잘 동작하는지 확인하는 과정을 e2e test로 대신할 수 있기 때문에, 이 작업이 많이 필요하다고 느꼈고 이것만 잘 구축된다면 제법 테스트하기 쉬운 앱이 될 것 같았다.

 

기존에 사용하려던 라이브러리는 detox였다. 이 라이브러리는 여러 가지 시나리오를 구체적으로 테스트할 수 있다는 장점이 있어서 이걸 써보고 싶었다. 그러나 현재 프로젝트에서는 react native 위에 expo를 사용하고 있는데, detox 공식문서에서는 공식적으로 expo와 같이 사용하기 위한 별도의 지원을 하고 있지는 않다고 했다. 즉 만약 오류가 발생하면 알아서 해결해야 한다는 것이다. 

 

그래서 고민했던 다른 옵션이 maestro였다. maestro는 detox와 달리 yaml 파일로 테스트 코드를 작성하며, 간편하게 작성할 수 있다는 장점이 있어 보였다. 그러나 detox와 달리 세부적인 케이스에 대해서 테스트를 하는 부분에서는 좀 약점이 있었다. 그리고 maestro로 테스트를 하려면 npm으로 패키지를 다운받는 게 아니라 CLI를 사용해야 한다는 것이 마음에 안 들었다. 

 

하지만 마음에 안 든다고 expo dependency를 이겨내면서 detox로 테스트하기에는 괜한 시간을 쓰는 것 같았기에, 일단 maestro로 진행해 보기로 했다. 

 

우선 아래와 같은 명령어로 CLI(iterm)에 maestro를 설치해 주자.

brew tap mobile-dev-inc/tap
brew install maestro

 

'maestro test'를 입력하면 CLI에서 자동으로 로컬에서 실행 중인 에뮬레이터를 찾는다고 한다. 

 

그렇다면 .yaml 파일은 어디에다 작성해야 할까? 'maestro test' 명령어 뒤에 테스트하고 싶은 yaml 파일의 경로를 입력하면 된다고 한다. 우리 프로젝트의 경우, 테스트 폴더에다 별도로 모든 파일들을 넣어두어야 할지, 아니면 각 view나 component마다 test 파일을 두어야 할지 고민이다. 이 부분은 팀원들에게 물어보고 결정해야겠다. 

 

그리고 구글링하다보니 도움이 되는 또 다른 글을 발견했다! react native를 사용해서 maestro로 end to end test를 하는 글이었다. 

 

빼먹은 부분이 있었는데, CLI에서 maestro 명령어를 자유롭게 사용하기 위해서 환경변수 설정을 해 주어야 했다. '.zshrc' 파일에 다음과 같은 명령어를 추가해 주자. 

export PATH="$PATH":"$HOME/.maestro/bin"

 

그리고 잘 동작하는지 확인해 보자. 우선 가장 간단하게는 로그인 화면을 테스트해볼 수 있겠다. 버튼을 누르면 구글 폼이 나타나는지를 테스트 해 보자. 그런데 아직 maestro yaml 파일 문법에 익숙하지 않아서, GPT 찬스로 예제 코드를 짜 달라고 했다. 

 

그리고 생각해보니 구글 로그인에 써 있는 텍스트가 다국어 버전별로 달랐다. 그래서 테스트할 때 다국어 상황에서 언어를 판단해서 특정 텍스트를 감지할 수 있는지도 궁금했다. 

이 부분은 로그인 버튼에 별도의 ID를 적용함으로써 알 수 있다고 했다. testID를 사용하면 버튼의 텍스트나 속성 등이 조금씩 달라지더라도 각 버튼을 고유하게 식별할 수 있다고 한다. 현재 앱에는 다국어 지원이 적용된 상황이라 텍스트만으로 버튼을 판단하기는 어려울 것 같아서, 해당 방법을 사용하기로 했다. 

 

그런데 구체적으로 어떻게 하는지를 보니 testID를 적용하려면 코드나 컴포넌트에 testID라는 속성을 하나씩 추가해 주어야 한다. 이러면 테스트 코드를 위해서 코드를 또 다 바꿔줘야 한다... 이게 맞을까? 어떻게 하면 코드의 변경을 최소화하면서 테스트 코드를 작성할 수 있을까?

 

여러 가지 방법이 있었다. 두 가지로 나눌 수 있겠다. 

  1. HOC(high order component)를 통해 코드의 변경을 최소화하면서 testId 적용하기
  2. 계층 구조를 사용해서 testID나 text를 사용하지 않고, 예를 들면 'container의 0번째 자식 컴포넌트' 이런 식으로 지정할 수도 있다고 한다. 

1번을 봤더니 여전히 코드에 손을 대야 하는 부분이 있었다. 가령 이런 식이었다. 

import React from 'react';
import { Button } from 'react-native';

const withTestID = (WrappedComponent, testID) => {
  return (props) => {
    return <WrappedComponent {...props} testID={testID} />;
  };
};

const GoogleLoginButton = withTestID(Button, 'googleLoginButton');

const LoginScreen = () => {
  return (
    <GoogleLoginButton title="Login with Google" onPress={() => {}} />
  );
};

export default LoginScreen;

 

HOC를 사용하는 방법의 경우, withTestID라는 함수를 통해 WrappedComponent를 변환해서 로직을 좀 통일해 주기는 했지만 여전히 테스트를 위해서 일반 로직에 손을 대야 하는 건 똑같았다. 그렇다고 계층 구조를 사용하자니, 만약 화면이나 계층 구조가 변화한다면 그에 맞춰서 테스트 코드도 수정해 주어야 했다. testID처럼 고유하게 컴포넌트를 식별하고는 싶지만, 그걸 일일이 앱 코드에다가 testID를 넣어서 주입해 주고 싶지는 않았다. 

 

다시 GPT 찬스를 써 보았다. 코드에 직접 testID라는 속성을 추가하지 않으면서도, 테스트 런타임에서만 해당 값을 추가해줄 수 있는 방법이 있었다. 바로 useEffect나 componentDidMount 훅을 사용해서, 테스트 환경에서 실행될 경우에만 testID 값을 추가해주는 방식이었다. 코드의 속성을 수작업으로 편집하지 않아서 괜찮은 방법 같았다. 

 

다만 여전히 많은 컴포넌트들이 있어서 이 함수를 하나씩 추가해야 한다는 점에서는 부담이 되었다. 문득 여러 규모가 있는 기업들은 이런 테스트를 어떻게 하고 있을지 궁금했다. 토스의 FE 아티클 하나를 참고해보니 위에서 잠깐 고민했던 것처럼 TestID는 어쨌거나 코드에 직접 주입을 해 주어야 하기 때문에 단점이 있다는 내용이 있었다. 

 

그리고 찾다 보니 토스페이먼츠 노션에서 어제 궁금한 점으로 적었던 mock과 stub의 차이점도 알 수 있었다. mock은 객체가 내부에서 외부로 나갈 때 상호작용을 모방하는 객체이고, stub은 외부에서 내부로 들어오는 객체의 상호작용을 모방하는 객체라고 한다. 예를 들면 mock은 이메일을 발송하는 등의 작업이고, stub은 데이터베이스에서 더미 데이터를 입력으로 받아오는 등의 작업이라고 한다. 

 

그러면 어떻게 테스트를 짜야 맞을까? 

 

다시 생각해보니 다국어 지원은 어쨌든 언어만 달라지는 것이므로 이 상황에서 크게 고려할 점은 아니었다. 만약 언어가 통일된다면, 기존과 같이 텍스트 방식으로 일단은 테스트를 작성해 보는것도 나쁘지 않겠다. 그러다가 추후에 변경을 해도 되겠다. 현재 i18n의 기본 설정으로는 fallbackLng(변환 실패했을 때의 기본 언어)가 영어로 되어 있어서, 영어를 기준으로 텍스트를 작성해주면 되겠다. 

 

아주아주 간단한 테스트를 작성해봤다. 잘 돌아가는지를 테스트 해 보려고 한다.

// indexTest.yaml
appId: com.safezone.onestep
---
# 앱을 실행
- launchApp

# 로그인 버튼을 찾고 클릭
- tapOn:
    text: 'Sign in with Google'

# Google 로그인 폼이 나타나는지 확인
- assertVisible:
    text: 'onestep'
maestro test indexTest.yaml

 

다음과 같은 명령어를 실행했더니, 이런 화면이 나왔다. 앱을 시작하는 것은 성공했으나 'Sign in with Google'이 들어간 텍스트를 찾지 못한 것 같았다. 

 

파란색 디렉토리로 가서 스크린샷을 확인해 보았다. 알고보니 앱을 런칭하면 바로 로그인 화면이 뜨는 게 아니라 다음과 같은 화면이 떠서, 'Sign in with Google' 텍스트를 찾지 못한 것 같았다. 

 

이 경우는 어떻게 해야 할지 고민이다. 일단 GPT 찬스를 써 보았다. 

 

이 부분은 Expo Go를 사용하는 것과 연관이 있다고 한다. 이 문제를 방지하려면 launchApp 명령어를 사용하기보다는 링크를 통해 직접 앱을 열어야 한다. 그런데 링크를 또 어떻게 찾나... GPT 피셜 exp://172.x.x.x 이런 링크를 찾으라는데, 아무리 봐도 콘솔에 그런 링크가 안 떴다. 

 

그래서 나름의 편법을 발견했는데, 바로 앱을 띄워두고 터미널을 하나 더 열어서 maestro로 테스트를 해 보았다. 그랬더니 아까는 fail했던 테스트가 잘 되더라. 

 

위의 코드로는 로그인 프로세스가 완료되지는 않았다. 아래와 같이 코드를 더 추가해서 진행해 주었고, 성공한 사진을 볼 수 있었다. 이제 이 테스트 코드를 로직의 첫 번째에 실행시켜서 나머지 시나리오들도 테스트해보면 되겠다. 

appId: com.safezone.onestep
---
# 앱을 실행
- launchApp

# 로그인 버튼을 찾고 클릭
- tapOn:
    text: 'Sign in with Google'

# Google 로그인 폼이 나타나는지 확인
- assertVisible:
    text: 'onestep'

# 로그인 폼이 나타나면 구글 계정 클릭
- tapOn:
    text: '.*gmail.com'
#
- tapOn:
    text: '계속'

- assertVisible:
    text: 'Today'
    waitUntilVisible: true

 

 궁금한 점

  1. componentDidMount와 useEffect 훅의 차이점은 무엇일까? 

 

 오늘 배운 것

pytest에서 그저께 작업한 내용을 바탕으로 pytest 명령어를 실행하였는데, 예상 외로 잘 되지 않았다. monkey patching을 통해서 해당 부분 코드를 스킵하는 부분이 잘 동작하지 않는 것 같았다. 나는 monkeypatch의 setattr 함수를 사용해서 해당 함수의 반환값을 None으로 바꾸려고 의도했었는데, 생각해보니 해당 함수를 호출하는 것 까지는 막을 수 없었을 수 있겠다. 

 

그러니까 이런 식이다. 

# conftest.py
@pytest.fixture(autouse=True)
def patch_send_push_notification_device(monkeypatch):
    monkeypatch.setattr('todos.firebase_messaging.send_push_notification_device', lambda: None)

 

즉 해당 함수의 리턴값은 None이 될 수 있다고 해도, 해당 함수를 호출하는 코드는 실행되고 있어서 해당 'request.auth.get("device")' 부분에서 에러가 나는 것 같다. 어떻게 하면 해당 코드를 아예 무시하고 호출되지 않도록 할 수 있을까? 그러니까 해당 함수(send_push_notification_device)의 인자로 들어가는 request.auth.get('device') 코드에서 문제가 발생하고 있었다. 

 

그래서 해결 방법을 알아보았다. 팀원의 조언을 들어보니 모든 테스트에서는 create_user와 authenticated_client라는 pytest fixture를 사용하고 있었다. 그래서 해당 authenticated_client fixture의 내용을 다음과 같이 수정해 주었더니 문제가 해결되었다. 

@pytest.fixture
def authenticated_client(create_user):
    # 원래 token={'device': None} 부분이 없었는데 추가해 주었음
    client.force_authenticate(user=create_user, token={"device": None})
    yield client
    client.force_authenticate(user=None)  # logout

 

여기서 client의 force_authenticate 메소드를 사용하면 해당 인자로 주어진 user와 token 값으로 강제로 인증을 시도한다. 내가 이해한 바로는 request 객체의 request.user 값과 request.auth 값을 주어진 값으로 강제로 바뀌게끔 한다. 

 

위와 같이 코드를 바꿔 주었더니 아래와 같이 기존엔 모두 fail하던 테스트 케이스들이 거의 다 통과되었다. 

 

일부 fail이 난 테스트 케이스는 비동기 뷰를 호출하는 테스트 케이스였다. 구체적인 로그는 다음과 같았다. coroutine과 관련된 에러가 나는 이유는 동기 상황을 가정하면 httpRequest를 리턴하기를 예상하는데, 비동기 뷰에서는 coroutine을 리턴해서 에러가 나는 것 같았다. 

 

팀원이 말해주길 'pytest asyncio'라는 라이브러리가 있다고 한다. 그래서 해당 라이브러리를 통해서 비동기 뷰를 테스트해보면 좋겠다. 일단 이 이슈는 따로 티켓을 파서 진행하자. 

 

궁금한 점

  1. 비동기 뷰의 동작 원리가 궁금하다. 나는 지금까지 def를 async def로 바꾸면 모든 문제가 해결! 되는 줄 알았는데 비동기 뷰의 동작 원리는 생각보다 복잡하더라... Celery는 왜 써야 할까? 그리고 왜 비동기 뷰가 있으면 비동기 요청을 핸들링할 수 있는 미들웨어가 있어야 효율적으로 동작할까? 
  2. monkey patching의 원리가 궁금하다.
  3. mock이랑 stup이 있다고 한다. stup은 뭘까
  4. pytest에서는 DRF의 authentication backend를 지나지 않는 걸까? 흐름이 궁금하다.

 

이번주는 취준과 프로젝트를 마치 청기백기 게임처럼 하느라 정신이 없었던 한 주였다. 

면접봤다가 코테봤다가 다시 인적성보다가 탑싯보고 코테보기

 

그러면서 앞으로 남은 10월과 11월을 어떻게 보낼지 고민을 많이 했다. 고민을 한 이유는 취준의 프로세스를 거치면서 생각보다 쏟아야 하는 노력들(코테 문제 풀기, 면접 대비해서 내가 한 것들 정리하기 등등)이 많았는데, 내가 막상 그만큼의 노력을 쏟지는 못하고 있었기 때문이다. 그러니까 두 마리 토끼(취준과 프로젝트)를 다 놓치고 있었다는 거다. 

 

게다가 이번주 초에는 Djangonaut에서 초대 메일도 받고, 앞으로의 일정과 가이드라인에 대한 공지도 떴었다. 그리고 우테코도 신청해뒀다. 이 일정들을 어느 정도 퀄리티 있게 유지하려면 뭔가는 내려놓아야 했다. 나는 그게 취준이라고 생각했다. 당연히 계속은 아니고 조금씩은 원서를 넣을 건데, 지금처럼 몇십 개씩 넣는 걸 안 하겠다는 이야기였다. 그러면 서류의 탈락과 합격 개수도 훨씬 줄어들 것이고, 조금이라도 더 여유롭게 임할 수 있을 것 같았다. 일주일에 최대 1-2개 넣는걸로 정해보면 좋을 것 같았다. 

 

이런 결정을 내리게 된 이유는 취준을 하는데 막상 취준의 비중이 너무 커지니 다른 것들의 퀄리티도 물론이고, 취준 자체의 퀄리티도 낮아진다고 스스로 많이 느꼈기 때문이다. 특히나 기출문제를 제대로 보지도 못한 상황에서 코테를 보는 상황이 많아지니, 이게 맞나? 라는 의구심이 들었다. 합격을 위해서는 조금의 준비는 필요한데, 나는 아예 준비도 못 하고 그냥 냅다 가서 코테를 보는 느낌이었다. 

 

그런데 사실 이 양치기 전략은 나의 불안에 기반한 거였다. 뭐라도 해야할 것 같아서 이곳 저곳 원서를 넣어봤고, 운이 좋게 서류를 보거나 면접을 본 곳도 있었다. 하지만 모수를 늘리기만 하는 것이 아니라, 모수를 늘려서 몇십 개에 기반한 데이터를 쌓았으면 이를 통해서 다시 전략을 세우는 것도 필요하겠다. 

 

Q. 지금 나의 전략은 무엇인가. 내가 우선으로 하는 것은?

  • 소마 프로젝트
  • Djangonaut
  • 우테코 프리코스
  • 취준 (1주일에 최대 2개)


Q. 9월-10월 동안의 데이터로 봤을 때 내가 좀 더 개선해야 되는 점은 무엇인가?

우선 코테를 매일 1문제씩 풀어야 함을 느꼈다. 그리고 코테의 목적이 명확하면 좋겠다는 생각이 들었다. 알고리즘 별로 복습을 할 것인지, 특정 기업에 합격하기 위한 문제를 풀 것인지에 따라서도 전략은 많이 달라지겠다. 내가 가고 싶은 기업들은 과제로 테스트를 하거나 코테를 엄청 어렵게 내는 기업은 아니기 때문에, 나는 유형별로 복습하는 것과 코테에서 Java 언어에 익숙해지는 것을 목표로 해야겠다. 

 

또한 면접 대비가 필요하다는 생각이 들었다. 막상 내가 한 프로젝트에서 뭔가를 질문을 받아도 관련된 개념을 정확하게는 몰랐던 적이 몇 번 있었다. 그리고 알고리즘 같은 기초 지식을 물어봤는데 헷갈린다는 점도 있었다. 다만 이 부분을 단순 암기로 접근하는 것은 아닌 것 같고, 내가 뭘 아는지에서부터 시작해서 확장하는 방식이 좋겠다. 조만간 이와 관련된 포스팅을 또 작성해야겠다. 

 

다음 주에는 이 사이클을 적용해 보고 느낀 점에 대해서 또 회고를 작성해 보자. 

 

 오늘 배운 것

어제 정리한 내용을 바탕으로 개발서버의 오류를 해결해보자. 현재 migration 관련해서 현재 브랜치와 테스트 서버의 버전이 일치하지가 않아서 모든 테스트에서 오류가 나는 상황이다. 하지만 이걸 일단 무시하고 테스트 코드를 개략적으로라도 작성해 보자. 

# todos/views.py
class TodoView(APIView):

    def post(self, request):
        if serializer.is_valid(raise_exception=True):
            serializer.save()

            send_push_notification_device(
                request.auth.get("device"),
                request.user,
                TODO_FCM_MESSAGE_TITLE,
                TODO_FCM_MESSAGE_BODY,
            )
            return Response(
                serializer.data, status=status.HTTP_201_CREATED
            )

 

현재 뷰의 코드 일부분을 가져와봤다. 여기서 send_push_notification_device는 알람을 보내는 함수이다. 이 로직은 FE 서버에서 요청이 들어오면 해당되는 device_token의 값이 있기에 정상적으로 호출되지만, 테스트 환경에서 호출되면 별도의 디바이스에서 호출되는 것이 아니므로 오류가 나는 문제가 있었다.

 

지금부터 할 일은 기존의 테스트 로직을 수정하여 해당 함수를 우회하도록 작성해 주는 것이다. 

 

우선 테스트에서는 django.urls의 reverse를 사용해서 이런 식으로 뷰의 이름을 통해 url을 가져온다. 여기서 url은 해당 뷰의 url이고, 'todos'는 해당 뷰를 unique하게 구분짓는 식별자로 볼 수 있다. 

from django.urls import reverse

url = reverse("todos")

 

맨 처음에 작성해 볼 todo의 create API를 보자. 해당 뷰의 이름은 'todos'로 되어 있다. 그렇다면 위와 같은 코드로 해당 API의 url을 얻을 수 있겠다. 이제 pytest의 patch 기능을 이용해서, 해당 뷰 함수 안에서 호출되는 함수가 리턴하는 값을 mock 객체로 바꿔 주면 되겠다. 

url = reverse('todos')
with('todos.firebase_messaging.send_push_notification_device', returned_value=None):
    response = client.get(url)

 

그런데 이 방법을 사용하기에는 바꿔줘야 할 테스트의 개수가 너무 많았다. 사실 열개 즈음이라 한다면 할 수도 있는데, 만약 여기서 로직 하나가 또 추가되어서 또 patch를 써야 하면 그것도 10개씩 추가해줘야 하는 상황이라 이런 방법을 최대한 안 쓰고 싶었다. 

 

그러니까 원하는 것은 'with patch'문을 일일이 선언하지 않아도 자동으로 특정 view를 호출할 때는 해당 뷰 안에 있는 특정 함수를 mock response 등으로 처리하는 거였다. 

 

찾아보니 방법이 있었다. pytest에서 '모든' 무언가에 기본적인 설정을 부여하고 싶을 때는 conftest.py 파일을 건드려야 한다는 것은 알고 있었는데, 이 방법도 마찬가지였다. 

# conftest.py

@pytest.fixture(autouse=True)
def patch_send_push_notification_device(monkeypatch):
    monkeypatch.setattr('todos.firebase_messaging.send_push_notification_device', lambda: None)

 

이렇게만 추가해 주면, 앞으로 어떤 뷰나 로직에서든 중간에 해당 send_push_notification_device 함수가 호출되는 부분이 있다면 해당 부분은 건너뛰어지고 리턴 값은 None으로 변환된다고 이해했다. 

 

다만 해당 fixture의 autouse 값이 참이기 때문에 만약 FCM 로직을 테스트하고 싶을 경우에는 수동으로 이를 disable 해 주어야 하겠다. 일단은 이대로 코드를 작성해 두었다. 

 

 궁금한 점

  1. mock 객체의 정확한 정의가 궁금하다.
  2. pytest의 patch 함수를 사용하면 로직이 어떻게 흘러갈까? 그리고 어떻게 해서 함수의 리턴값을 임의로 조작하는지도 궁금하다. 
  3. pytest의 monkeypatch는 또 무엇일까?

 

 오늘 배운 것

기존에 알림을 보내는 로직을 작업하면서, 요청이 들어왔을 때 해당 요청을 보낸 디바이스가 누군지 알아야 하는 이슈가 있었다. 그래서 이를 알기 위해서 request.auth의 속성 값으로 device_token 인자를 추가해주는 작업을 했었다. 

 

여기서 오류가 생겼다. 정확히는 테스트할 때 나는 오류였다.

현재는 해당되는 뷰(동기화용 알람이 필요한 뷰)의 맨 끝에다가 FCM 알림을 보내는 메소드 코드를 추가한 방식으로 구현했다. 그런데 이렇게 하면 테스트할 때도 해당 FCM 알림 로직을 지나게 된다.

 

여기서 두 가지의 이슈가 생긴다.

  1. 특정 뷰가 동작하는 것을 테스트할 때 FCM 알림이 보내지는 것까지 테스트하는 것은 불필요하다. 
  2. 테스트할 때 들어오는 request에는 별도로 fcm 디바이스 토큰을 넣어주지 않기 때문에 request.auth에 device token의 값이 없다. 그러므로 FCM 알림을 보내는 로직이 fail하게 된다. 

즉 뷰의 로직과 FCM 알림 로직을 분리해야 한다는 결론에 도달한다. 우회하는 방법도 있긴 하다. 가령 테스트할 때는 특정 파라미터의 값을 True로 바꿔 준 다음에, 해당 파라미터 값이 True이면 해당 알림 로직을 패스하는 방식으로 짤 수도 있다. 그러나 이는 일반적인 요청값 외에 추가로 파라미터를 넣어 줘야 하고, 결국 그 값 하나로 FCM 로직을 핸들링하는 것이기 때문에 적합하지 않다고 보았다. 

 

알고보니 pytest의 여러 기능 중에는 테스트하는 뷰 안의 특정 메소드가 호출되지 않도록 하는 기능도 있었다. 해당 기능을 사용해서 테스트 상황에서는 FCM 알림 관련 메소드를 호출하지 않도록 해 주면 되겠다.

 

근데 오늘은 자바 코테를 풀어야해서 내일 마저 해보겠다...!

 

 오늘 배운 것

개발서버가 고쳐졌다고 한다! 어제 작업한 다국어 기능에서 언어를 바꾸는 화면이 잘 동작하는지 확인해 보자. 

 

잘 동작한다. 다만 위에 나오는 'settingsLanguageView' 대신에 '언어 선택'이라고 나오도록 바꿔 보자. apps/_layout.tsx 파일(index.js와 같은 역할을 하는 루트 파일)에다가 다음과 같이 추가해 주었다. 

<Stack.Screen
  name="settingsLanguageView"
  options={{
  headerTitle: t('views.settingsView.language'),
  headerTitleAlign: 'center',
  }}
/>

 

그랬더니 잘 나오더라!

 

 궁금한 점

1. django mixins에 대해 궁금증이 생겼다. 발단은 팀원들과 React의 Compound Component 패턴에 대해서 얘기하면서부터였다. 가령 나는 이런 코드를 본 적이 있다. 

from rest_framework.mixins import ListModelMixin, CreateModelMixin
from rest_framework.generics import GenericAPIView


class ListCreateAPIView(GenericAPIView, ListModelMixin, CreateModelMixin):
	pass

 

여기서 GenericAPIView와 ListModelMixin, CreateModelMixin을 상속받아 APIView의 역할을 하면서도 ListView와 CreateView의 역할을 하는 View를 만들 수 있다고는 알고 있었다. 그런데 막상 Mixins가 구체적으로 왜, 어떻게 상속을 통해 GenericAPIView와 같이 동작할 수 있는지에 대해서는 구체적으로 생각하지 않은 것 같다. 다음에 이 부분에 대해서도 포스팅을 해봐야겠다. 

 

+ Recent posts