marlonyao 发表于 2013-1-30 01:54:54

django, mongodb与测试

在django下很容易写测试,只需要继承DjangoTestCase,它会自动创建一个测试数据库,每次运行时加载必要的fixture数据,以保证每个测试的初始状态是一致、可预测的。其前提是必须使用它的dbmodel,如果使用MySQL, Oracle等关系型数据库,这自然不是个问题。如果使用其它数据库,例如当前相当流行的NoSQL,这时DjangoTestCase就不能直接拿来用了。如果我们hack一下django,也是可以使用DjangoTestCase的。以mongodb为例,我使用的是django1.2,1.2以下的版本不能用这里的方式,我没有研究过,但我相信也是能hack的。

django有TestRunner,用来启动测试,加载fixture的工作就是在这里做的。默认的TestRunner是'django.test.simple.DjangoTestSuiteRunner',而它加载fixture的实际上调用的是loaddata命令。所以要实现fixture的加载工作,最简单的方式就是重新定义loaddata命令,让它将数据加载到mongodb中。随便选择一个app,在它的下面创建management目录,再在management下创建commands目录,然后再在其下创建loaddata.py,每个目录下面也都需要创建__init__.py文件。创建命令的工作可以参考这里。一般fixture的格式是用json格式,也可以用xml,写起来就会麻烦些,也可以使用普通文本格式,但解析起来就复杂了,并且不够灵活,因此推荐使用json。loaddata.py大概是这样:
from optparse import make_optionfrom django.core.management.base import BaseCommandfrom django.db.models import get_appsfrom django.utils import simplejson as jsonfrom pymongo.objectid import ObjectIdclass Command(BaseCommand):    help = 'Installs the named fixture(s) in the database.'    args = "fixture "    option_list = BaseCommand.option_list + (      make_option('--database', action='store', dest='database',            default='default db', help='Nominates a specific database to load '                'fixtures into. Defaults to the "default" database.'),    )    def handle(self, *fixture_labels, **options):      app_fixtures =       for fixture_label in fixture_labels:            for fixture_dir in app_fixtures:                fullpath = os.path.join(fixture_dir, fixture_label)                if os.path.isfile(fullpath):                  fixture = open(fullpath, 'r')                  data = norm_object(json.loads(fixture.read()))                  self._do_load(data)
由于json只有几种int, string, bool, array, dict几种数据类型,若是要代表datetime, ObjectId(这是mongodb中ID默认使用的类型)等复杂类型,就需要使用特别的表达方式,例如对datetime可以使用 { '$date': '2009/8/3 05:07:23' },对ObjectId可以使用 { '$oid': 'xxxxx' }来表示,这就需要做某种转换,这是在norm_object中完成的:
def norm_object(data):    if isinstance(data, dict):      if data.has_key('$oid'): # ObjectId            return ObjectId(data)      if data.has_key('$date'): # datetime            return parse_datetime(data['$date'])      if data.has_key('$ref'): #dbref            from pymongo.dbref import DBRef            return DBRef(data['$ref'], ObjectId(data['$id']))    if isinstance(data, dict):      return dict( [ (norm_object(k), norm_object(v)) for k, v in data.iteritems() ])    if isinstance(data, list):      return [ norm_object(o) for o in data ]    return data
对于_do_load方法,就是使用将fixture中的数据加载到mongodb中,没什么好说的。唯一需要说明的,就是如果指定将数据加载到哪个collection,我在fixture中每项数据中,除了需要加载到数据库的部分,还额外有个_collection属性,用来表明加载到哪个collection。一个用户数据的fixture可能会是这个样子:
[{"_collection": "user","_id" : { "$oid", "000011112222333344440001" },"username" : "marlon","email" : "marlon@163.com","password" : "sha1$6f90a$a1d2d0526aec9338e2d5ab7406315df849d9efdf","is_active" : true}]
_do_load方法实现如下:
    def _do_load(self, data):      db = get_db()      for obj in data:            col = obj.pop('_collection')            col = getattr(db, col)            col.save(obj, save=True)

到此为止,就实现了fixture加载的部分了。要每次运行测试之前加载fixture,在app目录下创建一个fixtures目录,再在其下创建相应的fixture,例如users.json。在TestCase中,fixtures数据指向fixture的文件名称:
class UserTestCase(DjangoTestCase):fixtures = [ 'users.json', 'other fixture...' ]

另外还需要在运行时创建测试数据库,在测试运行完成之后drop掉数据库。实现起来也很容易,只需要覆盖DjangoTestSuiteRunner的setup_databases和teardown_databases方法就可以了。
class MongoTestSuiteRunner(DjangoTestSuiteRunner):    def setup_databases(self, **kwargs):      self._test_dbname = 'test_' + settings.MONGODB_NAME      settings.MONGODB_NAME = self._test_dbname# do some database intialize work here# ...      return super(MongoTestSuiteRunner, self).setup_databases(**kwargs)    def teardown_databases(self, old_config, **kwargs):      conn = get connection ...      print 'drop mongo database %s...' % self._test_dbname      conn.drop_database(self._test_dbname)                super(MongoTestSuiteRunner, self).teardown_databases(old_config, **kwargs)

最后还需要在settings.py中指定testrunner为MongoTestSuiteRunner:
TEST_RUNNER = 'project.utils.test.SATestSuiteRunner'

完成这些工作之后就可以直接使用DjangoTestCase来写单元测试了,我就不教怎么写了。
页: [1]
查看完整版本: django, mongodb与测试