让Celery

时间:2017-10-02 17:40:33

标签: django testing celery

当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

我有两个问题:

  1. 如何让Celery可以看到Django测试用例的对象?

  2. 如何确保Celery保存的所有对象在测试完成后自动回滚?

  3. 如果无法自动执行此操作,我愿意手动清理对象,但即使在tearDownAPISimpleTestCase中的对象删除似乎也会被回滚。

3 个答案:

答案 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)