How should I test db service module with temp database?

Viewed 236

I'm looking for the right way to test service module with temporary database. I Have a working example, but I feel like there is a better solution out there

this is an example for testing a db_service function(much more of those to check), and I'm bit annoyed by the solution I came up with, I don't want to add those lines for each db_service function now. ~ you can see the lines I added in the diff between get_categories to original_get_categories.

eventually I want to create a solid behavior of using the original app session in default running and use the session to the temp database on testing.

models.py -

from sqlalchemy.orm import declarative_base


Base = declarative_base()


class Category(Base):
    __tablename__ = 'Categories'

    ID = Column(Integer, primary_key=True, autoincrement=True)
    Name = Column(Unicode(100))

db_service.py -

from models import Category
from app import session_scope  # this is the default session my service uses to query

class DBService:
    """provide CRUD operations"""
    
    @staticmethod
        def get_categories(session=None) -> List[Category]:
            if session is None:
                with session_scope() as session:
                    categories = session.query(Category).all()
                    return categories
            categories = session.query(Category).all()
            return categories

    @staticmethod
        def original_get_categories() -> List[Category]:
            with session_scope() as session:
                categories = session.query(Category).all()
                return categories

db_service_test.py -

from typing import List
from fixtures import db
from models import Category
from db_service import DBService


def test_get_all_categories(db):  # uses db fixture to create new database env
    first_category: Category = Category(ID=1, Name="first category")
    second_category: Category = Category(ID=2, Name="second category")
    session = db()
    session.add(first_category)
    session.add(second_category)
    session.commit()

    categories: List[Category] = DBService.get_categories(session)

    assert len(categories) == 2
    assert first_category in categories and second_category in categories

fixtures.py - it shouldn't be interesting but for any case

import pytest
from models import Base
from sqlalchemy.orm import scoped_session, sessionmaker


@pytest.fixture
def db():
    engine = create_engine('sqlite:///database.sqlite3')
    Base.metadata.create_all(bind=engine)
    db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))
    yield db_session
    Base.metadata.drop_all(engine)
    db_session().commit()

1 Answers

After spending some time on this I came up with 2 ideas.

First Option - setting init to DBService class

from models import Category


class DBService:
    """provide CRUD operations"""


    def __init___(self, session):
        self.session = session
    

    def get_categories() -> List[Category]:
        with self.session() as session:
            categories = session.query(Category).all()
            return categories

Now on the test module I just need to initialize DBService with the test session.

from typing import List
from fixtures import db
from models import Category
from db_service import DBService


def test_get_all_categories(db):  # uses db fixture to create new database env
    first_category: Category = Category(ID=1, Name="first category")
    second_category: Category = Category(ID=2, Name="second category")
    session = db()
    session.add(first_category)
    session.add(second_category)
    session.commit()

    test_service = DBService(db)
    categories: List[Category] = test_service.get_categories(session)

    assert len(categories) == 2
    assert first_category in categories and second_category in categories

Second Option - setting optional argument in every service function

from models import Category
from app import Session # this is the default session my service uses to query

class DBService:
    """provide CRUD operations"""
    
    @staticmethod
    def original_get_categories(session=Session) -> List[Category]:
        with session() as session:
            categories = session.query(Category).all()
            return categories

Now on the test module I just need to send my test db session to the functions.

from typing import List
from fixtures import db
from models import Category
from db_service import DBService


def test_get_all_categories(db):  # uses db fixture to create new database env
    first_category: Category = Category(ID=1, Name="first category")
    second_category: Category = Category(ID=2, Name="second category")
    session = db()
    session.add(first_category)
    session.add(second_category)
    session.commit()

    categories: List[Category] = DBService.get_categories(db) #  db, because session is not callable.

    assert len(categories) == 2
    assert first_category in categories and second_category in categories

Personally I choose the second option, I don't want to initialize DBService everywhere I use it.

Less changes in the code & one optional arguments can solve my problem.

if your service needs to hold more data then session than I would pick the first option, more organized.

Related