octabytes / FireO

Google Cloud Firestore modern and simplest convenient ORM package in Python. FireO is specifically designed for the Google's Firestore
https://fireo.octabyte.io
Apache License 2.0
247 stars 29 forks source link

[Question] How to test code that depends on Fireo in pytests? #226

Closed rommik closed 1 month ago

rommik commented 1 month ago

Hello FireO,

First of all thank you for this awesome tool. I've been using it for a few days and love how simple and intuitive it is.

I've tried to find a working example online, but without luck. I hope the community here can point me to the right answer.

what are the best practices or recommendations when testing (with "pytest") code that depends on FireO?

In My initial attempt, I tried to mock Firestore with "MockFirestore" and leave the rest as is. Unfortunately, any create/update operations fail with AttributeError: 'DocumentReference' object has no attribute 'path'. Did you mean: '_path'? it seems MockFirestore doesn't faithfully implement some internals for firebase_admin.firestore

Is there a better way?

Thanks.

ADR-007 commented 1 month ago

Hi @rommik! I used an emulator for local tests: docker-compose:

# ...
  firestore_emulator:
    image: mtlynch/firestore-emulator:latest
    environment:
      - FIRESTORE_PROJECT_ID=project-name
      - PORT=8200
    ports:
      - 8200:8200
# ...

def setup_remote_db(project_id: str):
    firebase_app = firebase_admin.initialize_app(
        firebase_admin.credentials.ApplicationDefault(),
        {'projectId': project_id},
    )
    client = firebase_admin.firestore.client(firebase_app)
    db.connect(client=client)

def setup_emulated_db(
    project: str = 'dummy',
    host: str = "0.0.0.0",
    port: int = 8200,
) -> None:
    os.environ["FIRESTORE_EMULATOR_HOST"] = f"{host}:{port}"
    os.environ["FIRESTORE_EMULATOR_HOST_PATH"] = f"{host}:{port}/firestore"

    credentials = Mock(spec=google.auth.credentials.Credentials)
    client = google.cloud.firestore.Client(project=project, credentials=credentials)
    db.connect(client=client)

def clean_emulated_db(
    project: str = 'dummy',
    host: str = "0.0.0.0",
    port: int = 8200,
) -> None:
    requests.delete(f"http://{host}:{port}/emulator/v1/projects/{project}/databases/(default)/documents")

@pytest.fixture(autouse=True, scope='session')
def setup_test_db():
    """Setup the database connection before the first test case."""
    setup_emulated_db(
        project=settings.FIRESTORE_EMULATOR_PROJECT,
        host=settings.FIRESTORE_EMULATOR_HOST,
        port=settings.FIRESTORE_EMULATOR_PORT,
    )
    yield

@pytest.fixture(autouse=True, scope='session')
def wait_for_emulator():
    """Wait for the database to be ready before each test case."""
    logger.info('Waiting for firestore emulator to be ready...')
    for _ in range(10):
        try:
            requests.get('http://{}:{}'.format(
                settings.FIRESTORE_EMULATOR_HOST,
                settings.FIRESTORE_EMULATOR_PORT,
            ))
            logger.info('Firestore emulator is ready.')
            return
        except requests.exceptions.ConnectionError:
            logger.info('Emulator is not ready yet...')
            sleep(1)

    raise Exception('Emulator is not ready')
@pytest.fixture(autouse=True, scope='session')
def clean_db_before_first_testcase() -> Generator[None, None, None]:
    assert isinstance(settings, FirestoreEmulatorSettings)
    clean_emulated_db(
        settings.FIRESTORE_EMULATOR_PROJECT,
        settings.FIRESTORE_EMULATOR_HOST,
        settings.FIRESTORE_EMULATOR_PORT,
    )
    yield

@pytest.fixture(autouse=True, scope='class')
def clean_db_after_testcase() -> Generator[None, None, None]:
    yield
    assert isinstance(settings, FirestoreEmulatorSettings)
    clean_emulated_db(
        settings.FIRESTORE_EMULATOR_PROJECT,
        settings.FIRESTORE_EMULATOR_HOST,
        settings.FIRESTORE_EMULATOR_PORT,
    )
class FirestoreEmulatorSettings(MyBasePydanticSettings):
    FIRESTORE_EMULATOR_HOST: str = '0.0.0.0'
    FIRESTORE_EMULATOR_PORT: int = 8200
    FIRESTORE_EMULATOR_PROJECT: str = 'my-project-name'
rommik commented 1 month ago

hi @ADR-007 thanks for your reply. Yeah, running Firestore in docker is an option too. I should have mentioned it. It complicates my CI/CD, but it's not a show-stopper. Thanks for the code you provided.