当Django测试用例运行时,它会创建一个独立的测试数据库,以便在每次测试完成时回滚数据库写入。我正在尝试与Celery一起创建集成测试,但我无法弄清楚如何将Celery连接到这个短暂的测试数据库。在天真的设置中,保存在Django中的对象对Celery是不可见的,并且Celery中保存的对象会无限期地存在。
以下是一个示例测试用例:
import json
from rest_framework.test import APITestCase
from myapp.models import MyModel
from myapp.util import get_result_from_response
class MyTestCase(APITestCase):
@classmethod
def setUpTestData(cls):
# This object is not visible to Celery
MyModel(id='test_object').save()
def test_celery_integration(self):
# This view spawns a Celery task
# Task should see MyModel.objects.get(id='test_object'), but can't
http_response = self.client.post('/', 'test_data', format='json')
result = get_result_from_response(http_response)
result.get() # Wait for task to finish before ending test case
# Objects saved by Celery task should be deleted, but persist
我有两个问题:
如何让Celery可以看到Django测试用例的对象?
如何确保Celery保存的所有对象在测试完成后自动回滚?
如果无法自动执行此操作,我愿意手动清理对象,但即使在tearDown
中APISimpleTestCase
中的对象删除似乎也会被回滚。
答案 0 :(得分:11)
这可以通过在Django测试用例中启动Celery工作程序来实现。
Django的内存数据库是sqlite3。正如它在the description page for Sqlite in-memory databases上所说的那样,“[A] ll共享内存数据库的数据库连接需要在同一个进程中。”这意味着,只要Django使用内存测试数据库并且Celery在一个单独的进程中启动,基本上就不可能让Celery和Django共享一个测试数据库。
但是,使用celery.contrib.testing.worker.start_worker
,可以在同一进程中的单独线程中启动Celery工作程序。该worker可以访问内存数据库。
这假设Celery已经在the usual way中设置了Django项目。
因为Django-Celery涉及一些跨线程通信,所以只有不在隔离事务中运行的测试用例才有效。测试用例必须直接从SimpleTestCase
或其Rest等效APISimpleTestCase
继承,并将类属性allow_database_queries
设置为True
。
关键是在setUpClass
的{{1}}方法中启动Celery工作者,并在TestCase
方法中关闭它。关键功能是tearDownClass
,它需要当前Celery应用程序的一个实例,可能是从celery.contrib.testing.worker.start_worker(app)
获得并返回一个Python mysite.celery.app
,其中ContextManager
和{{1} }方法,必须分别在__enter__
和__exit__
中调用。可能有一种方法可以避免使用装饰器或其他东西手动输入和存在setUpClass
,但我无法弄明白。以下是tearDownClass
文件的示例:
ContextManager
无论出于何种原因,测试工作者都会尝试使用名为tests.py
的任务,可能会在工作失败的情况下提供更好的错误消息。即使将from celery.contrib.testing.worker import start_worker
from django.test import SimpleTestCase
from mysite.celery import app
class BatchSimulationTestCase(SimpleTestCase):
allow_database_queries = True
@classmethod
def setUpClass(cls):
super().setUpClass()
# Start up celery worker
cls.celery_worker = start_worker(app)
cls.celery_worker.__enter__()
@classmethod
def tearDownClass(cls):
super().tearDownClass()
# Close worker
cls.celery_worker.__exit__(None, None, None)
def test_my_function(self):
# my_task.delay() or something
设置为'celery.ping'
作为关键字参数ot perform_ping_check
,仍会对其存在进行测试。它正在寻找的任务是False
。但是,默认情况下不安装此任务。通过在start_worker
中将celery.contrib.testing.tasks.ping
添加到celery.contrib.testing
,可以提供此任务。但是,这只会使工人看到它;而不是生成工人的代码。生成工作程序的代码执行INSTALLED_APPS
,但失败。对此进行评论会使一切正常,但修改已安装的库并不是一个好的解决方案。我可能做错了什么,但我确定的解决方法是将简单函数复制到settings.py
可以获取的某个地方,例如assert 'celery.ping' in app.tasks
:
app.autodiscover_tasks()
现在,在运行测试时,无需启动单独的Celery进程。 Celery工作程序将作为一个单独的线程在Django测试过程中启动。该worker可以看到任何内存数据库,包括默认的内存中测试数据库。为了控制工作人员数量,celery.py
中有可用的选项,但看起来默认是单个工作人员。
答案 1 :(得分:4)
对于您的单元测试,我建议跳过芹菜依赖,以下两个链接将为您提供启动您的单元测试的必要信息:
如果你真的想测试包含队列的celery函数调用,我可以设置一个dockercompose与服务器,worker,队列组合,并从django-celery docs扩展自定义CeleryTestRunner。但我不会从中看到它带来的好处,因为测试系统远离生产而具有代表性。
答案 2 :(得分:0)
我发现了基于@drhagen的解决方案的另一种解决方法:
在致电celery.contrib.testing.app.TestApp()
之前先致电start_worker(app)
from celery.contrib.testing.worker import start_worker
from celery.contrib.testing.app import TestApp
from myapp.tasks import app, my_task
class TestTasks:
def setup(self):
TestApp()
self.celery_worker = start_worker(app)
self.celery_worker.__enter__()
def teardown(self):
self.celery_worker.__exit__(None, None, None)