Python鼻子测试,SQLAlchemy和“便利功能”

时间:2015-02-27 16:06:10

标签: python unit-testing transactions sqlalchemy nose

我有一个关于如何设计好的Nose单元测试(使用每个测试事务和回滚)的问题,不仅围绕SQLAlchemy模型,还围绕创建SQLAlchemy模型的便利函数。

我对如何编写基本单元测试类有了一个很好的理解,其中包括必要的设置和拆卸夹具来包装事务中的所有测试,并在测试完成后回滚它们。但是,到目前为止,所有这些测试都涉及直接创建模型。例如,像这样测试User模型(BaseTestCase包含setup / teardown fixture):

from Business.Models import User

class test_User(BaseTestCase):

    def test_user_stuff(self):
        user = User(username, first_name, last_name, ....)
        self.test_session.add(user)
        self.test_session.commit()
        # do various test stuff here, and then the
        # transaction is rolled back after the test ends

但是,我还编写了一个包含创建User对象的便捷函数。它处理各种事情,如确认密码匹配验证密码,然后创建盐,散列密码+盐等,然后将这些值放入User表/对象的相关列/字段中。它看起来像这样:

def create_user(username, ..., password, password_match):

    if password != password_match:
        raise PasswordMatchError()

    try:
        salt = generate_salt()
        hashed_password = hash_password(password, salt)

        user = User(username, ..., salt, hashed_password)
        db_session.add(user)
        db_session.commit()

    except IntegrityError:
        db_session.rollback()
        raise UsernameAlreadyExistsError()

    return user

现在,我也想对这个函数进行单元测试,但是我不确定在使用测试数据库实现的单元测试用例中包装它的正确方法,在每次测试后回滚事务等等。 / p>

from Business.Models.User import create_user

class test_User(BaseTestCase):

    def test_create_user_stuff(self):
        user = create_user(username, first_name, last_name, ....)
        # do various test stuff here
        # Now how do I finangle things so the transaction
        # executed by create_user is rolled back after the test?

提前感谢您的帮助,并指出我正确的方向。

2 个答案:

答案 0 :(得分:1)

以下是两种可能的方法:

在测试之间重新创建数据库

setUp()代码中,从空白数据库开始,创建您正在测试的对象所需的表。在tearDown()中,请自行清理。

您可以创建一个基本测试类,如:

class SqlaTestCase(unittest.TestCase):
    db_url = 'sqlite:///:memory:'
    auto_create_tables = True

    def setUp(self):
        self.engine = create_engine(self.db_url)
        self.connection = self.engine.connect()

        if self.auto_create_tables:
            self.create_tables()

        Session = sessionmaker(bind=self.connection)
        self.session = Session()

    def tearDown(self):
        self.session.close_all()
        if self.auto_create_tables:
            self.drop_tables()
        self.connection.close()
        self.engine.dispose()

    def create_tables(self):
        self.Base.metadata.create_all(self.engine)

    def drop_tables(self):
        self.Base.metadata.drop_all(self.engine)

然后每个测试都不必担心遗留任何数据。

不要在不同级别重新测试相同的功能

如果您有良好的测试覆盖率,则此助手调用的基础功能已经过测试。因此,您可以专注于确保在此级别拥有正确的逻辑。

模拟会话(甚至是密码哈希功能)并进行测试以确保进行了适当的调用(例如assert_called_with())。

答案 1 :(得分:1)

在我们讨论适合您的应用的单元测试模式之前,我可以推荐一些东西(这将显着简化您的应用流程和测试):

  1. 不要来自create_user()的session.commit(),或者实际上来自您创建的任何便利函数或方法,即用session.flush()替换session.commit()来保存数据w / o提交交易。

  2. 修改验证异常(例如UsernameAlreadyExistsError)以确保它在出错时执行session.rollback()。如果某些验证失败,这将保证您不会提交整件事。

  3. session.commit()一旦在最后,处理整个请求并传递所有方法和验证。例如,这可以通过从控制器调用commit()来实现,例如,请求终止。或者(这就是我所做的),你可以从对象析构函数中调用commit()。你可以更进一步,并引入一个必须由调用者显式传递的commit = 1 | 0标志(如果现在传递了这个标志,默认情况下提交)。

  4. 如果这种方法适合你,突然你的unittest实现变得更加清晰。首先,实现基本单元测试类,它将被其他每个单元测试覆盖,例如

    from Business.Models.User import create_user
    
    class BaseTestCase(unittest2.TestCase):
    
      @classmethod
      def setUpClass(cls):
        cls.session = ... # get your already init'ed session, init it otherwise (normally it should be done once)
        # other common statements
        ... 
    
      def setUp(self):
        self.session.rollback()
        ...
    
      def tearDown(self):
        self.session.rollback()
        ...
    
    class UserTest(BaseTestCase):
    
      @classmethod
      def setUpClass(cls):
        super(UserTest, cls).setUpClass()
        ...
    
      def setUp(self):
        super(UserTest, self).setUp()
        ...
    
      def tearDown(self):
        super(UserTest, self).tearDown()
    
      def test_create_user_success(self):
        user = create_user('john_smith', 'John', 'Smith')
        self.session.commit()
        user_was_created = ... # verify
    
      def test_create_user_error(self):
        user = create_user('john_smith', 'John', 'Smith')
        self.session.commit()
        with self.assertRaisesRegexp(UsernameAlreadyExistsError, "john_smith already exists"):
          user = create_user('john_smith', 'John', 'Smith')