시작 세팅

장고

대략적인 설명만 하자면 공모전을 위해 장고를 공부하게 되었고, 도커를 활용하여 장고 환경을 세팅하였다.

docker-compose run --rm app sh -c "python manage.py startapp core" 를 통해 core 앱을 만들수 있는데 그 이후 해줘야하는 작업이 있다.

장고는 settings.py에 있는 INSTALLED_APPS 만 마이그레이션, URL 매핑, 모델 등록 등에서 인식한다고 한다

따라서 추가한 core를 INSTALLED_APPS에 추가해줘야한다

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'core',  # core 앱을 추가
]

Race Condition 해소

Docker-Compose 로 service와 db 두개의 컨테이너를 관리할때 service 컨테이너에서 db에 접근할때 아직 db는 준비가 안된 상태일수도 있다

따라서 wait_for_db.py 를 통해 타이밍을 맞추어 줘야한다. 다시 말해 wait_for_db를 service 컨테이너에서 계속 실행을 해야한다는 말인데 그러기 위해서는 커스텀 명령어 지정을 해줘야한다

그리고 Django에서 커스텀 명령어를 실행하려면 반드시 management/commands 경로 아래에 파일이 위치해야한다

title: 꺠알 상식
Python에서 ***arg(별하나)**는 **가변인자**, **\**opt(별두개)**는 **키워드인자**를 받을수 있다는 표현이다
#### **✅ 실제 사용 예시**
```bash
`python manage.py custom_command 123 --verbose=True`
```
- `123` → `args`에 들어감 (`args = (123,)`)
- `--verbose=True` → `opt`에 들어감 (`opt = {'verbose': True}`)
@patch('core.management.commands.wait_for_db.Command.check')  # ✅ Command.check()를 Mock 처리
class CommandTests(SimpleTestCase):
    """Test commands"""
 
    def test_wait_for_db_ready(self, patched_check):  # ✅ patched_check는 Mock 객체
        """Test waiting for database if database ready"""
        patched_check.return_value = True  # ✅ check()가 항상 True를 반환하도록 설정
 

@patch() 데코레이터가 자동으로 Command.check를 Mock 객체로 바꿔주고, 이를 테스트 메서드(test_wait_for_db_ready)의 인자로 넘겨주기 때문에 patched_check이 Mock객체가 되고 Mock 객체 내부의 return_value를 통해 호출시 True를 반환하게 해줄수 있다

테스트 설명

return_value

@patch('core.management.commands.wait_for_db.Command.check')  # ✅ Command.check()를 Mock으로 대체
class CommandTests(SimpleTestCase):
    """Test commands"""
 
    def test_wait_for_db_ready(self, patched_check):
        """Test waiting for database if database ready"""
        patched_check.return_value = True  # ✅ check()가 항상 True를 반환하도록 설정
 
        call_command('wait_for_db')  # ✅ wait_for_db 실행 (내부적으로 check() 호출)
 
        patched_check.assert_called_once_with(databases=['default'])  # ✅ check()가 단 한 번 호출되었고, 올바른 인자로 호출되었는지 확인
 

기존 Command.check는 실제 데이터베이스 를 체크를하여 작동준비가 되면 True를 반환하게끔 한다. 테스트에서는 이 check을 Mock 객체로 받아, 실제 데이터베이스를 참고하게 하지 않고 미리 patched_check.return_value = True를 통해 준비가 되었다는 가정을 깔고 wait_for_db를 호출하였다. (호출시 해당 값 반환) 그리고 assert함수를 통해 wait_for_db가 호출되면 databases=['defailt'] 가 세팅될것이므로 이를 통해 테스트를 하는 것이다

side_effect

@patch('time.sleep')  # ✅ time.sleep()을 Mock으로 대체 (테스트 속도 향상을 위해)
def test_wait_for_db_delay(self, patched_sleep, patched_check):
    """Test waiting for database when getting Operational Error"""
    patched_check.side_effect = [Psycopg2Error] * 2 + \
                                [OperationalError] * 3 + \
                                [True]  # ✅ 2번 Psycopg2Error, 3번 OperationalError, 마지막으로 True 반환
 
    call_command('wait_for_db')  # ✅ wait_for_db 실행 (내부적으로 check() 반복 호출)
 
    self.assertEqual(patched_check.call_count, 6)  # ✅ check()가 총 6번 호출되었는지 검증
    patched_check.assert_called_with(databases=['default'])  # ✅ check()가 올바른 인자로 호출되었는지 확인
 

side_effect는 Mock객체가 호출될 때 실행할 동작의 순서를 지정하는 속성이다. 위 예시의 경우 2번의 Psycopg2에러, 3번의 Operational 에러 그리고 True를 반환하게 하여 5번동안 준비가 안되다가 6번째에 준비가 된 시나리오를 만든것이다

그리고 time.sleep같은 경우 Mock을 하게되면 따로 함수에 작성하지 않아도 테스트에서 sleep은 즉시반환되거나 사용자가 지정한 방식대로 조절할수 있다.