如何正确实现有状态Python模块的测试隔离?
我正在做的项目是一个商业逻辑软件,封装成一个Python包。这个包的想法是,其他脚本或应用程序可以导入它,初始化后使用它。
目前,它有一个顶层的init()方法,用于初始化和设置各种东西。比如,它会设置SQLAlchemy的数据库连接,并存储SA会话,以便后续使用。这个会话被存储在我项目的一个子包中(也就是myproj.model.Session),这样其他代码在导入模型后就可以获取一个有效的SA会话。
长话短说,这让我的包变成了一个有状态的包。我正在为这个项目编写单元测试,而这种有状态的行为带来了一些问题:
- 测试应该是独立的,但我包的内部状态破坏了这种独立性。
- 我无法测试主要的init()方法,因为它的行为依赖于状态。
- 未来的测试需要在(尚未编写的)控制器部分上运行,并且需要一个已知的模型状态(例如,一个预填充的sqlite 内存数据库)。
我是否应该以某种方式重构我的包,因为当前的结构不是最佳实践? :)
我是否应该就这样保持现状,每次都设置和拆除整个东西?如果我想实现完全的独立性,那就意味着在每个测试中完全清除并重新填充数据库,这样做是不是太过了?
这个问题主要是关于整体代码和测试结构,但为了说明,我在测试中使用的是nose-1.0。我知道Isolate插件可能会对我有所帮助,但我想在测试套件中做一些奇怪的事情之前,先把代码理顺。
3 个回答
在一个设置比较复杂的项目中(比如使用IPython),我看到一种方法是调用一个叫做get_ipython
的函数。这个函数的作用是设置并返回一个实例,同时它自己会被替换成一个新的函数,这个新函数会返回已经存在的实例。这样,每个测试都可以调用这个函数,但只有第一个测试会进行设置。
这样做的好处是,避免了每个测试都要重复进行繁琐的设置过程。不过,有时候会出现一些奇怪的情况,比如某个测试的结果会因为之前运行过的测试而有所不同。我们有一些方法来处理这种情况——很多测试应该在任何状态下都能得到相同的结果,我们可以在某些测试之前尝试重置对象的状态。你可能会发现类似的权衡对你也有帮助。
你有几个选择:
模拟数据库
这里有一些需要注意的权衡。
你的测试会变得更复杂,因为你需要进行设置、清理和模拟连接。你可能还想验证发送的SQL或命令。这种方法通常会导致一种奇怪的紧密耦合,这可能会让你在数据库结构或SQL发生变化时,花更多时间来维护和更新测试。
不过,这种方式通常是测试隔离最纯粹的形式,因为它减少了测试中可能存在的大依赖。它还可以让测试运行得更快,减少在持续集成环境中自动化测试套件的负担。
每次测试都重建数据库
同样需要注意一些权衡。
这可能会让你的测试变得非常慢,具体取决于重建数据库需要多长时间。如果开发数据库服务器是共享资源,那么每个开发者都需要在服务器上有自己的数据库,这会需要额外的初始投资。服务器的性能可能会受到影响,尤其是测试运行得很频繁的时候。在持续集成环境中运行测试套件时,也会增加额外的负担,因为可能需要多个数据库(具体取决于同时构建的分支数量)。
不过,这种方法的好处在于可以实际运行与生产环境中相同的代码路径和类似的资源。这通常有助于更早地发现bug,这总是件好事。
ORM数据库切换
如果你使用像SQLAlchemy这样的ORM(对象关系映射),你可以将底层数据库切换为可能更快的内存数据库。这可以帮助你减轻前面两种选择的一些缺点。
虽然这不是生产环境中使用的同样的数据库,但ORM应该能帮助降低隐藏bug的风险。通常,设置一个内存数据库的时间要比设置一个文件支持的数据库短得多。而且,它的好处在于只与当前的测试运行相关,所以你不需要担心共享资源管理或最后的清理工作。