Django queryset 结果对于测试是错误的

Django queryset result is wrong for the test

我的模型是:

class AndroidOffer(models.Model):
    name = models.CharField(max_length=128, db_index=True)
    # ...
    countries = models.ManyToManyField(Country)

以及以下代码(我跳过了之前的过滤):

active_offers = active_offers.filter(countries__in=[country])

它生成这个 SQL 查询:

SELECT "offers_androidoffer"."id", "offers_androidoffer"."name", "offers_androidoffer"."title", "offers_androidoffer"."is_for_android", "offers_androidoffer"."is_for_ios", "offers_androidoffer"."url", "offers_androidoffer"."icon", "offers_androidoffer"."cost", "offers_androidoffer"."quantity", "offers_androidoffer"."hourly_completions", "offers_androidoffer"."is_active", "offers_androidoffer"."description", "offers_androidoffer"."comment", "offers_androidoffer"."priority", "offers_androidoffer"."offer_type", "offers_androidoffer"."package_name", "offers_androidoffer"."is_search_install", "offers_androidoffer"."search_query", "offers_androidoffer"."launches" FROM "offers_androidoffer" INNER JOIN "offers_androidoffer_platform_versions" ON ("offers_androidoffer"."id" = "offers_androidoffer_platform_versions"."androidoffer_id") INNER JOIN "offers_androidoffer_countries" ON ("offers_androidoffer"."id" = "offers_androidoffer_countries"."androidoffer_id") WHERE ("offers_androidoffer"."is_active" = True AND "offers_androidoffer"."quantity" > 0 AND NOT ("offers_androidoffer"."id" IN (SELECT U0."offer_id" FROM "offers_androidofferstate" U0 WHERE (U0."device_id" = 1 AND (U0."state" = 3 OR U0."state" = 4)))) AND NOT ("offers_androidoffer"."package_name" IN (SELECT V0."package_name" FROM "applications_app" V0 INNER JOIN "applications_deviceapp" V1 ON (V0."id" = V1."app_id") WHERE (V1."device_id" IN (SELECT U0."device_id" FROM "users_userdevice" U0 WHERE U0."user_id" = 2) AND NOT (V0."package_name" IN (SELECT U2."package_name" FROM "offers_androidofferstate" U0 INNER JOIN "offers_androidoffer" U2 ON (U0."offer_id" = U2."id") WHERE (U0."device_id" = 1 AND (U0."state" = 0 OR U0."state" = 1 OR U0."state" = 2))))))) AND "offers_androidoffer_platform_versions"."platformversion_id" IN (14) AND "offers_androidoffer_countries"."country_id" IN (6252001)) ORDER BY "offers_androidoffer"."priority" DESC;

如果我在 Postgresql 控制台中 运行 这个查询,它将 return 0 行,但是 active_offers 有 4 个结果(table 中的所有行),就像我删除了 AND "offers_androidoffer_countries"."country_id" IN (6252001) 语句。

我 运行 此代码来自测试(APITestCase.client -> DRF 视图 -> 过滤查询集)。 Django 版本是 2.0.2.

为什么它忽略国家/地区过滤?

UPD. 我刚刚检查了简单的 TestCase(测试 -> 过滤查询集)测试,它 return 的行数正确。因此,问题仅存在于 DRF 测试中。

UPD 2. 工作不正确的测试用例:

class AndroidOffersListTests(APITestCase):
    fixtures = [
        'geo/fixtures/cities.json',
        'offers/fixtures/users.json',
        'offers/fixtures/devices.json',
        'offers/fixtures/geo.json',
        'offers/fixtures/apps.json',
        'offers/fixtures/offers.json',
    ]

    def test_list_offers_1(self):
        user_device = UserDevice.objects.get(pk=1)

        token = AndroidOffersListTests.get_token_for_device(user_device)
        self.client.credentials(HTTP_AUTHORIZATION='Token {}'.format(token))

        url = AndroidOffersListTests.get_url(user_device)
        response = self.client.get(url)

        self.assertEqual(status.HTTP_200_OK, response.status_code)
        self.assertEqual(0, len(response.data)) # result is 4

查看代码:

class AndroidOffersView(ListAPIView):
    model = AndroidOffer
    serializer_class = AndroidOffersSerializer
    permission_classes = (IsAuthenticated,)

    def get_queryset(self):
        device = UserDevice.get_from_request(self.request)
        if device is None:
            raise PermissionDenied()

        return AndroidOffer.get_offers_for_device(device)

get_offers_for_device:

@staticmethod
def get_offers_for_device(user_device):
    active_offers = AndroidOffer.get_active_offers()

    # Filter completed
    completed_states = AndroidOfferState.get_completed_for_device(user_device)
    completed_offers_ids = completed_states.values_list('offer__pk', flat=True)

    active_offers = active_offers.exclude(pk__in=completed_offers_ids)

    # Filter apps already installed on the user's devices
    apps = user_device.user.apps

    # Remove packages that are in progress
    in_progress_states = AndroidOfferState.get_in_progress_for_device(user_device)
    in_progress_packages = in_progress_states.values_list('offer__package_name', flat=True)
    apps = apps.exclude(package_name__in=in_progress_packages)

    packages = apps.values_list('package_name', flat=True)

    active_offers = active_offers.exclude(package_name__in=packages)

    # Filter by platform version
    active_offers = active_offers.filter(platform_versions__in=[user_device.device.version])

    # Filter by country
    country = user_device.last_geo_record.country
    if country is not None:
        active_offers = active_offers.filter(countries__in=[country])

    return active_offers

运行良好的测试用例:

class AndroidOffersListTests(TestCase):
    fixtures = [
        'geo/fixtures/cities.json',
        'offers/fixtures/users.json',
        'offers/fixtures/devices.json',
        'offers/fixtures/geo.json',
        'offers/fixtures/apps.json',
        'offers/fixtures/offers.json',
    ]

    def test_list_offers_1(self):
        user_device = UserDevice.objects.get(pk=1)

        offers = AndroidOffer.get_offers_for_device(user_device)

        self.assertEqual(0, offers.count()) # 0 — thats ok

更新 3: 当我在浏览器中 运行 发送相同的请求时,它工作正常:

field__in 检查字段是否在您传递给它的列表中。

你可以通过这个获得你想要的行为

active_offers = active_offers.filter(countries=country)

你说这个回复不正确:

self.assertEqual(0, len(response.data)) # result is 4

但你也说这个 JSON 回复是 正确的:

{
    "count": 0,
    "next": null,
    "previous": null,
    "results": []
}

您在这里使用的是 分页 API4 的长度是由于反序列化 json 中存在的键数:

>>> len(json.loads('{"count": 0, "next": null, "previous": null, "results": []}'))
4

请注意,您不需要自己实际调用 json.loads,DRF 框架在准备响应时已经为您处理了这个问题 - 即 response.data 已经是一个字典。

在 "Test case where it works fine" 中,您直接处理查询集:

self.assertEqual(0, offers.count()) # 0 — thats ok
                         ^
                         |____ here you go to the database, no serializer!

如果您想查看分页 JSON api 中的结果数量,则需要向下钻取该页面:

len_results = len(response.data['results'])

对于预期 return 0 个结果的测试,这就足够了。但要小心 - 如果您曾经有过希望生成比页面大小(在设置中配置)更多结果的测试,您可能还需要检查 countnext 值。您必须向后续页面发出额外请求才能收集所有结果。