앞 단계 참조 링크:
- 상태 코드: 2024.03.26 - [Web 개발/FAST API (인프런 강의 내용)] - 1 FastAPI 알아보기
- 프로젝트 소개 및 환경 구축: 2024.04.05 - [Web 개발/FAST API (인프런 강의 내용)] - 1 실습1 GET API 전체조회
- 1 실습1 GET API ToDo 전체 조회: 2024.04.05 - [Web 개발/FAST API (인프런 강의 내용)] - 1 실습1 GET API 전체조회
- 1 실습2 GET API ToDo 단일 조회: 2024.04.09 - [Web 개발/FAST API (인프런 강의 내용)] - 1 실습2 GET API 단일조회
- 1 실습3 POST API ToDo 생성: 2024.04.15 - [Web 개발/FAST API (인프런 강의 내용)] - 1 실습3 POST API todo 생성
- 1 실습4 PATCH API ToDo 수정: 2024.04.16 - [Web 개발/FAST API (인프런 강의 내용)] - 1 실습4 PATCH API todo 수정
- 1 실습5 DELETE API ToDo 삭제: 2024.04.17 - [Web 개발/FAST API (인프런 강의 내용)] - 1 실습5 DELETE API todo 삭제
- 1 실습6 ERROR 처리: 2024.04.18 - [Web 개발/FAST API (인프런 강의 내용)] - 1 실습6 ERROR 처리
- 2 데이터베이스: 2024.04.24 - [Web 개발/FAST API (인프런 강의 내용)] - 2 데이터베이스
- 2 실습1 MySQL 컨테이너 실행 (docker): 2024.04.24 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습1 MySQL 컨테이너 실행 (docker)
- 2 실습2 MySQL 접속 및 사용: 2024.04.25 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습2 MySQL 접속 및 사용
- 2 실습3 데이터베이스 연결: 2024.04.25 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습3 데이터베이스 연결
- 2 실습4 ORM 모델링: 2024.05.02 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습4 ORM 모델링
- 2 실습5 ORM GET 전체조회 API: 2024.05.03 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습5 ORM 적용 - GET 전체조회 API
- 2 실습6 ORM HTTP Response 처리: 2024.05.10 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습6 ORM 적용 - HTTP Response 처리
- 2 실습7 ORM GET 단일조회 API: 2024.05.14 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습7 ORM 적용 - GET 단일조회 API
- 2 실습8 ORM Refactoring: 2024.06.01 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습8 ORM 적용 - Refactoring
- 2 실습9 ORM POST API: 2024.06.01 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습9 ORM 적용 - POST API
- 2 실습10 ORM PATCH API: 2024.06.01 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습10 ORM 적용 - PATCH API
- 2 실습11 ORM DELETE API: 2024.06.01 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습11 ORM 적용 - DELETE API
- 3 테스트 코드 PyTest: 2024.06.01 - [Web 개발/FAST API (인프런 강의 내용)] - 3 테스트 코드 PyTest
- 3 실습1 PyTest 세팅: 2024.07.22 - [Web 개발/FAST API (인프런 강의 내용)] - 3 실습1 PyTest 세팅
- 3 실습2 테스트 코드 GET 전체조회 API: 2024.08.01 - [Web 개발/FAST API (인프런 강의 내용)] - 3 실습2 테스트 코드 - GET 전체조회 API
- 3 실습3 테스트 코드 PyTest Mocking: 2024.08.02 - [Web 개발/FAST API (인프런 강의 내용)] - 3 실습3 테스트 코드 - PyTest Mocking
- 3 실습4 테스트 코드 PyTest Fixture: 2024.08.03 - [Web 개발/FAST API (인프런 강의 내용)] - 3 실습4 테스트 코드 - PyTest Fixture
- 3 실습5 테스트 코드 GET 단일조회 API: 2024.08.05 - [Web 개발/FAST API (인프런 강의 내용)] - 3 실습5 테스트 코드 - GET 단조회 APIsf
- 3 실습6 테스트 코드 POST API: 2024.08.05 - [Web 개발/FAST API (인프런 강의 내용)] - 3 실습6 테스트 코드 - POST API
- 3 실습7 테스트 코드 PATCH API: 2024.08.05 - [Web 개발/FAST API (인프런 강의 내용)] - 3 실습7 테스트 코드 - PATCH API
- 3 실습8 테스트 코드 DELETE API: 2024.08.07 - [Web 개발/FAST API (인프런 강의 내용)] - 3 실습8 테스트 코드 - DELETE API
- 4 실습1 Refactoring FastAPI Router: 2024.08.08 - [Web 개발/FAST API (인프런 강의 내용)] - 4 실습1 Refactoring - FastAPI Router
- 4 실습2 Refactoring 의존성 주입: 2024.08.08 - [Web 개발/FAST API (인프런 강의 내용)] - 4 실습2 Refactoring - Dependency Injection 의존성 주입
- 4 실습3 Refactoring 레포지토리 패턴: 2024.08.08 - [Web 개발/FAST API (인프런 강의 내용)] - 4 실습3 Refactoring - Repository Pattern 레포지토리 패
- 5 기능 고도화 SQL JOIN: 2024.08.11 - [Web 개발/FAST API (인프런 강의 내용)] - 5 기능 고도화 SQL JOINㄹ
- 5 실습1 User 테이블 모델링: 2024.08.12 - [Web 개발/FAST API (인프런 강의 내용)] - 5 실습1 기능 고도화 User 모델링
- 5 실습2 User 테이블 생성: 2024.08.15 - [Web 개발/FAST API (인프런 강의 내용)] - 5 실습2 기능 고도화 User 테이블 생성
- 5 실습3 ORM JOIN: 2024.10.16 - [Web 개발/FAST API (인프런 강의 내용)] - 5 실습3 ORM JOIN
- 5 기능 고도화 Lazy Loading / Eager Loading: 2024.10.18 - [Web 개발/FAST API (인프런 강의 내용)] - 5 기능 고도화 Lazy Loading / Eager Loading
- 5 실습4 회원가입 API 생성 & 비밀번호 암호화(bcrypt): 2024.10.18 - [Web 개발/FAST API (인프런 강의 내용)] - 5 실습4 회원가입 API 생성 & 비밀번호 암호화(bcrypt)
- 5 실습5 회원가입 API 구현: 2024.10.18 - [Web 개발/FAST API (인프런 강의 내용)] - 5 실습5 회원가입 API 구현
00 개요
- 회원가입 API의 테스트 코드 작성하는 실습 진행
01 회원가입 API 테스트 코드 작성
1. test_users_api.py에 추가
- 경로: todos/src/tests/test_users_api.py
- 변경 사항:
- body = {"username": "test", "password": "plain"} 추가
- response = client.post("users/sign-up")에 json=body 추가
- → response = client.post("users/sign-up", json=body )
- 상태 코드 200을 201로 수정
- → assert response.status_code == 201
1) UserService.hash_password 메소드에 Mocking 적용
- mocking 적용 이유:
- 회원가입 API를 보면, 전달받은 plain_password를 해시해주는 과정이 포함됨
- 바로 다음과 같은 부분임 (api/user.py)
# /c/Users/관리자/Desktop/projects/todos/src/api/user.py 내용
...
@router.post("/sign-up", status_code=201)
def user_sign_up_handler(
request: SignUpRequest,
user_service: UserService = Depends(),
user_repo: UserRepository = Depends()
):
# 해싱된 비밀번호 생성 # 여기
hashed_password: str = user_service.hash_password(
plain_password = request.password
)
...
# /c/Users/관리자/Desktop/projects/todos/src/service/user.py 내용
import bcrypt
class UserService:
encoding: str = "UTF-8"
def hash_password(self, plain_password: str) -> str:
'''
input:
- plain_password: string
output:
- hashed_password: string
function: receives plain-text password and returns hashed password
'''
hashed_password: bytes = bcrypt.hashpw(
plain_password.encode(self.encoding),
salt=bcrypt.gensalt()
)
return hashed_password.decode(self.encoding)
- api/user.py의 '해싱된 비밀번호 생성' 부분은 salt로 인해 약간의 random성 동작을 함 (service/user.py의 UserService 참조)
- 이 부분(UserService.hash_password)을 항상 일정하게 검증하기 어려워서 일단은 mocking 적용하는 것
- 원래 이런 개별 메소드에 대해 동작 검증 시 unit test로 별도의 검증 방법을 사용하지만
- 우리가 할 API 검증은, API의 flow만 검증하는 것이 목적이기에
- 우리는 일단 이 메소드 (hash_password)가 정상적으로 동작한다는 가정 하에 API 검증을 진행하는 것 → 그리하여 다음과 같이 적용!
# /c/Users/관리자/Desktop/projects/todos/src/tests/test_users_api.py 내용
from src.service.user import UserService # import
def test_user_sign_up(client, mocker): # mocker는 fixture
hashed_password = mocker.patch.object( # UserService.hash_password 메소드에 mocking 적용
UserService,
"hash_password",
return_value="hashed"
)
body = { # API에 넘겨줄 body 추가
"username": "test",
"password": "plain"
}
response = client.post("users/sign-up", json=body) # json 형식으로 body 넘겨줘서 POST 요청 보내기 추가
hashed_password.assert_called_once_with( # UserService.hash_password 검증
plain_password="plain"
)
assert response.status_code == 201 # 201로 변경
assert response.json() is True
- 변경 사항:
- mocking 적용하기 위해 def test_user_signup(client)에 mocker라는 fixture 추가
- → def test_user_signup(client, mocker)
- mocker의 patch의 object를 사용해 UserService에 있는 hash_password 메소드를 모킹해주기, 이것을 hash_password 변수에 담아주기
- →hash_password = mocker.patch.object(UserService, "hash_password", return_value="hasehd")
- 만약에 hash_password 메소드가 정상적으로 동작을 했다면 우리가 return_value로 "plain"이 아니라 해시된 값을 받아야 되어서 "hashed"라는 return_value를 지정
- →hash_password = mocker.patch.object(UserService, "hash_password", return_value="hasehd")
- 방금 만든 hash_password가 assert_called_once_with를 사용하여 우리가 전달한 plain_password를 통해 호출되었는지 검증할 것
- → hash_password.assert_called_once_with(plain_password="plain")
- 전에는 해당 mocker가 호출됐는지 여부만 확인하는 assert_called_once만 사용했었는데, assert_called_once_with는 구체적으로 어떤 인자를 가지고 호출됐는지 여부 확인이 가능
- api/user.py에 보면, plain_password에 request에 전달한 password가 잘 담겨서 호출되었는지 확인해보는 것
- plain_password 키로 password ("plain")가 잘 전달 되었는지 확인해보는 것
- mocking 적용하기 위해 def test_user_signup(client)에 mocker라는 fixture 추가
2) User.create 메소드에 Mocking 적용
# /c/Users/관리자/Desktop/projects/todos/src/api/user.py 내용
...
@router.post("/sign-up", status_code=201)
def user_sign_up_handler(
request: SignUpRequest,
user_service: UserService = Depends(),
user_repo: UserRepository = Depends()
):
...
# 사용자 생성 # 여기
user: User = User.create(
username = request.username,
hashed_password = hashed_password
)
...
- user_create 라는 변수에 mocker.patch.object를 사용해서 User 클래스의 create 메소드 모킹해줘서 담기
- → user_create = mocker.patch.pbject(User, "create", return_value=User(id=None, username="test", password="hased"))
- 반환값은 User 모델인데 이 시점에는 아직 id가 None, username은 밑에 지정된대로 "test", password도 밑에 지정된대로 "hashed"가 될 것
- 방금 만든 user_create 가 assert_called_once_with를 사용하여 username은 우리가 request로 전달한 "test"가 넘어가야 되고 hashed_password는 우리가 방금 hash_password의 return_value="hashed"값이 잘 넘어오는 것을 검증할 것
- → user_create.assert_called_once_with(username="test", hased_password="hashed")
→ 그리하여 다음과 같이 적용!
# /c/Users/관리자/Desktop/projects/todos/src/tests/test_users_api.py 내용
from src.service.user import UserService
from src.database.orm import User # import
def test_user_sign_up(client, mocker): # mocker는 fixture
hashed_password = mocker.patch.object( # UserService.hash_password 메소드에 mocking 적용
UserService,
"hash_password",
return_value="hashed"
)
user_create = mocker.patch.object( # User.create 메소드에 mocking 적용
User,
"create",
return_value=User(id=None, username="test", password="hashed")
)
body = { # API에 넘겨줄 body 추가
"username": "test",
"password": "plain"
}
response = client.post("users/sign-up", json=body) # json 형식으로 body 넘겨줘서 POST 요청 보내기 추가
hashed_password.assert_called_once_with( # UserService.hash_password 검증
plain_password="plain"
)
user_create.assert_called_once_with( # User.create 검증
username="test", hashed_password="hashed"
)
assert response.status_code == 201 # 201로 변경
assert response.json() is True
3) UserRepository.save_user 메소드에 Mocking 적용
# /c/Users/관리자/Desktop/projects/todos/src/api/user.py 내용
...
@router.post("/sign-up", status_code=201)
def user_sign_up_handler(
request: SignUpRequest,
user_service: UserService = Depends(),
user_repo: UserRepository = Depends()
):
...
# 사용자를 DB에 저장 # 여기
user: User = user_repo.save_user(user=user) # 이 시점에서 사용자의 id 값 = int로 들어감, 실제 DB에 사용자가 생성됨
...
- user_save 라는 변수에 mocker.patch.object를 사용해서 UserRepository 클래스의 save_user 메소드 모킹해줘서 담기
- → user_save = mocker.patch.pbject(UserRepository , "save_user ", return_value=User(id=1, username="test", password="hased"))
- 반환값은 User.create 메소드와 같이 User 모델인데 이 시점에는 id에 값이 할당 되어 1이 될 것, username은 밑에 지정된대로 "test", password도 밑에 지정된대로 "hashed"가 될 것
- 최종 반환값을 "id": 1, "username": "test", "password": "hashed"가 되도록 확인하기
- → assert response.json() == {"id": 1, "username": "test"}
→ 그리하여 다음과 같이 적용!
# /c/Users/관리자/Desktop/projects/todos/src/tests/test_users_api.py 내용
from src.service.user import UserService
from src.database.orm import User
from src.database.repository import UserRepository # import
def test_user_sign_up(client, mocker): # mocker는 fixture
hashed_password = mocker.patch.object( # UserService.hash_password 메소드에 mocking 적용
UserService,
"hash_password",
return_value="hashed"
)
user_create = mocker.patch.object( # User.create 메소드에 mocking 적용
User,
"create",
return_value=User(id=None, username="test", password="hashed")
)
mocker.patch.object( # UserRepository.save_user 메소드에 mocking 적용
UserRepository,
"save_user",
return_value=User(id=1, username="test", password="hashed")
)
body = { # API에 넘겨줄 body 추가
"username": "test",
"password": "plain"
}
response = client.post("users/sign-up", json=body) # json 형식으로 body 넘겨줘서 POST 요청 보내기 추가
hashed_password.assert_called_once_with( # UserService.hash_password 검증
plain_password="plain"
)
user_create.assert_called_once_with( # User.create 검증
username="test", hashed_password="hashed"
)
assert response.status_code == 201 # 201로 변경
assert response.json() == {"id": 1, "username": "test"} # 반환값으로 UserRepository.save_user 검증
2. test_users_api.py 실행
- pytest로 검증 실행
- -v 옵션: verbose, 더 많은 정보를 출력함 (실패 또는 에러에 관한 정보 출력)
# pytest test_users_api.py -v
- 실패 및 에러남!
- 오류 설명:
- sqlalchemy.exc.IntegrityError: 데이터베이스에 중복 데이터가 삽입되려고 할 때 발생하는 오류
- 이 오류는 SQLAlchemy에서 발생한 IntegrityError로, username 필드에 대해 중복된 값('test')이 삽입되려고 했기 때문에 발생한 것
- Duplicate entry 'test' for key 'user.username': 'test'라는 값이 user.username 필드에 이미 존재하여, 해당 값을 다시 삽입할 수 없다는 오류
- MySQL 데이터베이스에서 username 필드에 대해 유일성 제약 조건이 설정되어 있기 때문에, 중복된 값이 삽입되면 오류가 발행
- sqlalchemy.exc.IntegrityError: 데이터베이스에 중복 데이터가 삽입되려고 할 때 발생하는 오류
- 해결 방법
- 중복된 사용자명 확인: 테스트 데이터베이스에서 'test'라는 사용자명이 이미 존재하는지 확인 후 존재 시 다른 사용자명을 사용하도록 수정
- 또 다른 실패 및 에러 남!
- 오류 설명
- AssertionError: Expected 'hash_password' to be called once. Called 0 times: hash_password 메서드가 예상대로 호출되지 않았다는 것을 의미
- 이 문제는 mocker.patch로 모킹된 메서드가 테스트 실행 중에 호출되지 않았기 때문에 발생
- 즉, UserService.hash_password가 호출되지 않음
- hash_password 메서드는 사용자의 비밀번호를 해싱하는 역할을 함
- 만약 UserService.hash_password가 호출되지 않는다면, 테스트 코드에서 실제로 hash_password 메서드를 호출하지 않거나 호출되지 않는 로직이 실행된 것
- AssertionError: Expected 'hash_password' to be called once. Called 0 times: hash_password 메서드가 예상대로 호출되지 않았다는 것을 의미
- 해결 방법 - 실패
- 1. hash_password 호출이 제대로 이루어지는지 확인:
- hash_password가 호출되는 코드 흐름을 다시 점검
- 예를 들어, UserService.hash_password를 호출하는 로직이 test_user_sign_up 함수 내에서 잘 동작하는지 확인
- hash_password가 호출되는 코드가 빠져 있거나 다른 곳에서 호출되지 않는 경우가 있을 수 있음
- 2. 로깅 추가하여 호출 여부 확인:
- hash_password 메서드 내에 로그를 추가하여 호출 여부를 실시간으로 확인 가능
- 예를 들어:
- 1. hash_password 호출이 제대로 이루어지는지 확인:
import logging
class UserService:
encoding: str = "UTF-8"
def hash_password(self, plain_password: str) -> str:
logging.info("hash_password called with: %s", plain_password)
hashed_password: bytes = bcrypt.hashpw(
plain_password.encode(self.encoding),
salt=bcrypt.gensalt()
)
return hashed_password.decode(self.encoding)
- 해결 방법 - 실패
- 3. mocker.patch 위치 확인:
- mocker.patch가 제대로 적용되는지 확인
- hash_password 메서드가 호출되기 전에 mocker.patch가 적용되어야 함
- test_user_sign_up 내에서 mocker.patch가 올바르게 실행되고 있는지 확인
- 4. User.create 메서드가 hash_password를 호출하는지 확인:
- hash_password 메서드는 User.create나 다른 로직 내에서 호출될 가능성이 있음
- 이 부분이 제대로 연결되지 않으면 hash_password가 호출되지 않을 수 있음
- 예를 들어, User.create가 hash_password를 호출하는지 확인
- 3. mocker.patch 위치 확인:
- 못 고침..인줄 알았으나!!!
- 해결 방법 - main.py에서 import들 경로가 바뀌어있었음!
- 3. Swagger Ui에서 확인 - 참조
- 해결 못할 줄 알고 Swagger Ui에서도 한 번 확인해보자 했었는데
- sign-up API에서 Internal Server Error가 뜸, 이것은 보통 파일 자체가 안 될 때 나는 에러라서 해당 함수가 호출되는 main.py으로 가보니 import 경로에서 다 src.가 빠져있던 것..! src.를 다 추가해주고 Swagger Ui에서 작동시켜보니 되고, pytest 도 원활하게 passed! 가 뜸!!!
- 3. Swagger Ui에서 확인 - 참조
# cd todos/src/tests
# pytest test_users_api.py -v
- 검증이 잘 통과된 것을 확인!
3. Swagger UI에서 확인
- uvicorn 실행!
# uvicorn main:app --reload --port 8003
- /docs로 들어가서 관리자 화면에서 /users/sign-up 클릭 하여 sign-up 요청 실습해보기
- /users/sign-up > Try it out > Request Body에 {"username": "test2", "password": "plain_password"} 입력 > Execute
- 실패... 왜 이럴까
- 코드를 다시 보아하니... main.py에서 import들을 할 때 경로가 잘 참조되지 않았었음..!
- 앞에 src.가 안 붙어있었음!!! (언제 이걸 뺐었는지 기억도 안 남,,)
- src.붙여주고 다시 Swagger UI 에서 실행해보니 됨!
- 위를 아래와 같이 변경 후 저장
- Swagger UI에서 잘 됨!
- pytest도 잘 됨..
- 내가 못살아 진짜... 원인을 몰라서 몇 시간동안 고민했는지 몰라 진짜....
'Web 개발 > FAST API (인프런 강의 내용)' 카테고리의 다른 글
5 실습7 로그인 API 구현 (1) | 2024.11.16 |
---|---|
5 기능 고도화 - 로그인 / 유저 인증 - JWT (0) | 2024.11.16 |
5 실습5 회원가입 API 구현 (0) | 2024.10.18 |
5 실습4 회원가입 API 생성 & 비밀번호 암호화(bcrypt) (1) | 2024.10.18 |
5 기능 고도화 Lazy Loading / Eager Loading (0) | 2024.10.18 |