我试图为Cython模块设置单元测试来测试一些没有python接口的函数。第一个想法是检查.pyx
的测试运行员是否可以直接使用py.test
文件,但显然它只扫描.py
个文件。
第二个想法是在一个Cython模块中编写test_*
方法,然后可以将其导入到普通的.py
文件中。假设我们有一个foo.pyx
模块,其中包含我们要测试的内容:
cdef int is_true():
return False
然后使用C API测试test_foo.pyx
模块的foo
模块:
cimport foo
def test_foo():
assert foo.is_true()
然后在只包含此行的普通cython_test.py
模块中导入它们:
from foo_test import *
py.test
测试运行器以这种方式找到test_foo
,但随后报告:
/usr/lib/python2.7/inspect.py:752: in getargs
raise TypeError('{!r} is not a code object'.format(co))
E TypeError: <built-in function test_foo> is not a code object
有没有更好的方法来使用py.test
测试Cython C-API代码?
答案 0 :(得分:5)
所以,最后,我设法让py.test
直接从Cython编译的.pyx
文件中运行测试。然而,这种方法是一种可怕的黑客攻击,旨在尽可能地使用py.test
Python测试运行器。它可能会停止使用与我准备工作的任何py.test
版本不同的版本(即2.7.2)。
首先是打败py.test
关注.py
文件。最初,py.test
拒绝导入任何没有.py
扩展名文件的内容。另外一个问题是py.test
验证模块的__FILE__
是否与.py
文件的位置匹配。 Cython的__FILE__
通常不包含源文件的名称。我不得不重写这个检查。我不知道这种覆盖是否会破坏任何东西 - 我只能说测试似乎运行良好,但如果您担心,请咨询您当地的py.test
开发人员。此部分是作为本地conftest.py
文件实现的。
import _pytest
import importlib
class Module(_pytest.python.Module):
# Source: http://stackoverflow.com/questions/32250450/
def _importtestmodule(self):
# Copy-paste from py.test, edited to avoid throwing ImportMismatchError.
# Defensive programming in py.test tries to ensure the module's __file__
# matches the location of the source code. Cython's __file__ is
# different.
# https://github.com/pytest-dev/pytest/blob/2.7.2/_pytest/python.py#L485
path = self.fspath
pypkgpath = path.pypkgpath()
modname = '.'.join(
[pypkgpath.basename] +
path.new(ext='').relto(pypkgpath).split(path.sep))
mod = importlib.import_module(modname)
self.config.pluginmanager.consider_module(mod)
return mod
def collect(self):
# Defeat defensive programming.
# https://github.com/pytest-dev/pytest/blob/2.7.2/_pytest/python.py#L286
assert self.name.endswith('.pyx')
self.name = self.name[:-1]
return super(Module, self).collect()
def pytest_collect_file(parent, path):
# py.test by default limits all test discovery to .py files.
# I should probably have introduced a new setting for .pyx paths to match,
# for simplicity I am hard-coding a single path.
if path.fnmatch('*_test.pyx'):
return Module(path, parent)
第二个主要问题是py.test
使用Python的inspect
模块来检查单元测试的函数参数的名称。请记住,py.test
这样做可以注入固定装置,这是一个非常漂亮的功能,值得保留。 inspect
不适用于Cython,并且通常似乎没有简单的方法可以使原始inspect
与Cython一起使用。也没有任何其他好的方法来检查Cython函数的参数列表。现在我决定做一个小的解决方法,我将所有测试函数包装在一个具有所需签名的纯Python函数中。
除此之外,似乎Cython会自动为每个__test__
模块添加.pyx
属性。 Cython的做法干扰py.test
,需要修复。据我所知,__test__
是Cython的内部细节,没有任何地方暴露,所以我们覆盖它并不重要。在我的例子中,我将以下函数放入.pxi
文件中以包含在任何*_test.pyx
文件中:
from functools import wraps
# For https://github.com/pytest-dev/pytest/blob/2.7.2/_pytest/python.py#L340
# Apparently Cython injects its own __test__ attribute that's {} by default.
# bool({}) == False, and py.test thinks the developer doesn't want to run
# tests from this module.
__test__ = True
def cython_test(signature=""):
''' Wrap a Cython test function in a pure Python call, so that py.test
can inspect its argument list and run the test properly.
Source: http://stackoverflow.com/questions/32250450/'''
if isinstance(signature, basestring):
code = "lambda {signature}: func({signature})".format(
signature=signature)
def decorator(func):
return wraps(func)(eval(code, {'func': func}, {}))
return decorator
# case when cython_test was used as a decorator directly, getting
# a function passed as `signature`
return cython_test()(signature)
之后,我可以实现以下测试:
include "cython_test_helpers.pxi"
from pytest import fixture
cdef returns_true():
return False
@cython_test
def test_returns_true():
assert returns_true() == True
@fixture
def fixture_of_true():
return True
@cython_test('fixture_of_true')
def test_fixture(fixture_of_true):
return fixture_of_true == True
如果您决定使用上述黑客攻击,请记住给自己留下评论,并附上此答案的链接 - 我会尽量保持更新以备更好的解决方案。