对依赖于数据库的功能进行单元测试

时间:2018-09-12 17:06:06

标签: python unit-testing python-unittest database-testing

我正在对某些功能进行测试。我有一个使用数据库查询的功能。因此,我浏览了博客和文档,说我们必须创建一个内存中或测试数据库才能使用这些功能。下面是我的功能,

def already_exists(story_data,c):
    # TODO(salmanhaseeb): Implement de-dupe functionality by checking if it already
    # exists in the DB.
    c.execute("""SELECT COUNT(*) from posts where post_id = ?""", (story_data.post_id,))
    (number_of_rows,)=c.fetchone()
    if number_of_rows > 0:
        return True
    return False

此功能访问生产数据库。我的问题是,在测试中,我创建了一个内存数据库并在其中填充值,我将在查询该数据库(测试数据库)。但是我想测试我的already_exists()函数,从测试中调用我的already_exists函数后,我的生产数据库将被命中。在测试此功能时如何使测试数据库命中?

2 个答案:

答案 0 :(得分:0)

您可以采取两种方法来解决此问题:

  1. 进行集成测试而不是单元测试,只需使用真实数据库的副本
  2. 向方法提供伪造品,而不是实际的连接对象。

您应该选择哪一项取决于您要实现的目标。

如果要测试查询本身是否有效,则应使用集成测试。句号确保查询符合预期的唯一方法是使用数据库副本中已经存在的测试数据来运行查询。针对 数据库技术(例如,在PostgreSQL中的生产数据库时针对SQLite运行)无法确保其在生产环境中正常运行。需要数据库的副本意味着您将需要一些自动部署过程,该过程可以轻松地针对单独的数据库进行调用。无论如何,您应该具有这样一个自动化的过程,因为它有助于确保跨环境的部署是一致的,允许您在发布之前对其进行测试,并“记录”升级数据库的过程。对此的标准解决方案是使用您的编程语言编写的迁移工具(例如albemic或执行原始SQL的工具(例如yoyoFlyway)。您需要在运行测试之前调用部署并用测试数据填充它,然后运行测试并声明您希望返回的输出。

如果要测试查询 around 的代码,而不是查询本身,则可以对连接对象使用伪造的代码。最常见的解决方案是a mock。 Mocks提供了standins,可以将其配置为接受函数调用和输入并返回一些输出来代替实际对象。假设查询返回了您期望的结果,这将允许您测试方法的逻辑是否正确运行。对于您的方法,这样的测试可能看起来像这样:

from unittest.mock import Mock

...

def test_already_exists_returns_true_for_positive_count():
    mockConn = Mock(
        execute=Mock(),
        fetchone=Mock(return_value=(5,)),
    )
    story = Story(post_id=10) # Making some assumptions about what your object might look like.

    result = already_exists(story, mockConn)

    assert result

    # Possibly assert calls on the mock. Value of these asserts is debatable.
    mockConn.execute.assert_called("""SELECT COUNT(*) from posts where post_id = ?""", (story.post_id,))
    mockConn.fetchone.assert_called()

答案 1 :(得分:-1)

问题是确保您的代码始终使用相同的数据库连接。然后您可以将其设置为适合当前环境的任何一次。

与其在方法之间传递数据库连接,不如将它变成单例。

def already_exists(story_data):
    # Here `connection` is a singleton which returns the database connection.
    connection.execute("""SELECT COUNT(*) from posts where post_id = ?""", (story_data.post_id,))
    (number_of_rows,) = connection.fetchone()
    if number_of_rows > 0:
        return True
    return False

或者在每个类上将connection用作方法,并将already_exists变成方法。无论如何,它可能应该是一种方法。

def already_exists(self):
    # Here the connection is associated with the object.
    self.connection.execute("""SELECT COUNT(*) from posts where post_id = ?""", (self.post_id,))
    (number_of_rows,) = self.connection.fetchone()
    if number_of_rows > 0:
        return True
    return False

但是实际上您不应该自己滚动这段代码。相反,您应该使用ORM之类的SQLAlchemy这样的基本查询和连接管理。它只有一个连接"session"

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from sqlalchemy_declarative import Address, Base, Person

engine = create_engine('sqlite:///sqlalchemy_example.db')
Base.metadata.bind = engine

DBSession = sessionmaker(bind=engine)
session = DBSession()

然后使用它进行查询。例如,it has an exists method

session.query(Post.id).filter(q.exists()).scalar()

使用ORM将极大地简化您的代码。这是a short tutorial for the basics和一个longer and more complete tutorial