Как организовать уровень доступа к базе данных?

Я использую SqlAlchemy, библиотеку ORM на питоне. И я использовал для доступа к базе данных непосредственно из бизнес-уровня напрямую, вызывая API SqlAlchemy.

Но потом я обнаружил, что это вызовет слишком много времени для запуска всех моих тестовых примеров, и теперь я думаю, что, возможно, мне следует создать уровень доступа к БД, поэтому я могу использовать макетные объекты во время теста, а не напрямую обращаться к базе данных.

Я думаю, что есть 2 варианта:

  • используйте один класс, который содержит соединение с БД и многие методы, такие как addUser/delUser/updateUser, addBook/delBook/updateBook. Но это означает, что этот класс будет очень большим.

  • Другим подходом является создание различных классов менеджера, таких как "UserManager", "BookManager". Но это означает, что мне нужно передать список менеджеров в бизнес-уровень, что кажется немного громоздким.

Как вы упорядочиваете слой базы данных?

Ответ 1

Это хороший вопрос!
Проблема не тривиальна и может потребовать нескольких подходов к ее решению. Например:

  • Организуйте код, чтобы вы могли протестировать большую часть логики приложения, не обращаясь к базе данных. Это означает, что каждый класс будет иметь методы для доступа к данным и методы его обработки, а второй может быть легко протестирован.
  • Когда вам нужно проверить доступ к базе данных, вы можете использовать прокси (так, как решение # 1); вы можете думать об этом как о двигателе для SqlAlchemy или о замене SA на замену. В обоих случаях вы можете подумать о self initializing fake.
  • Если код не содержит хранимых процедур, подумайте об использовании баз данных в памяти, как говорит Леннарт (даже если в этом случае его "unit test" может показаться немного странным!).

Однако, по моему опыту, все довольно легко по слову, а затем резко падает, когда вы идете на поле. Например, что делать, если большая часть логики находится в операторах SQL? Что делать, если доступ к данным строго чередуется с его обработкой? Иногда вы можете реорганизовать, иногда (особенно с большими и устаревшими приложениями).

В конце концов, я думаю, что это в основном вопрос мышления.
Если вы считаете, что вам нужны модульные тесты, и вам нужно, чтобы они работали быстро, то вы разрабатываете свое приложение определенным образом, что позволяет упростить модульное тестирование.
К сожалению, это не всегда так (многие люди рассматривают модульные тесты как то, что может работать в одночасье, поэтому время не является проблемой), и вы получаете то, что не будет действительно тестируемым по отдельности.

Ответ 2

Я бы установил соединение с базой данных во время тестирования, которое вместо этого подключается к базе данных в памяти. Например:

sqlite_memory_db = create_engine('sqlite://')

Это будет очень быстро, как вы можете получить, вы также не подключаетесь к реальной базе данных, а просто временны в памяти, поэтому вам не нужно беспокоиться об изменениях, сделанных вашими испытаниями, оставшимися после тест и т.д. И вам не нужно ничего издеваться.

Ответ 3

Один из способов захвата изменений в базе данных - использовать механизм расширения сеанса SQLAlchemy и перехватывать сбросы в базу данных, используя что-то вроде этого:

from sqlalchemy.orm.attributes import instance_state
from sqlalchemy.orm import SessionExtension

class MockExtension(SessionExtension):
    def __init__(self):
        self.clear()

    def clear(self):
        self.updates = set()
        self.inserts = set()
        self.deletes = set()

    def before_flush(self, session, flush_context, instances):
        for obj in session.dirty:
            self.updates.add(obj)
            state = instance_state(obj)
            state.commit_all({})
            session.identity_map._mutable_attrs.discard(state)
            session.identity_map._modified.discard(state)

        for obj in session.deleted:
            self.deletes.add(obj)
            session.expunge(obj)

        self.inserts.update(session.new)
        session._new = {}

Затем для тестов вы можете настроить сеанс с этим макетом и посмотреть, соответствует ли он вашим ожиданиям.

mock = MockExtension()
Session = sessionmaker(extension=[mock], expire_on_commit=False)

def do_something(attr):
    session = Session()
    obj = session.query(Cls).first()
    obj.attr = attr
    session.commit()

def test_something():
    mock.clear()
    do_something('foobar')
    assert len(mock.updates) == 1
    updated_obj = mock.updates.pop()
    assert updated_obj.attr == 'foobar'

Но вы все равно захотите сделать хотя бы некоторые тесты с базой данных, потому что вы по крайней мере хотите знать, работают ли ваши запросы так, как ожидалось. И имейте в виду, что вы также можете внести изменения в базу данных через session.update(), .delete() и .execute().

Ответ 4

В SQLAlchemy есть некоторые возможности для упростить насмешку - может быть, это было бы проще, чем пытаться переписать целые разделы вашего проекта?