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": []
}
您在这里使用的是 分页 API。 4
的长度是由于反序列化 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 个结果的测试,这就足够了。但要小心 - 如果您曾经有过希望生成比页面大小(在设置中配置)更多结果的测试,您可能还需要检查 count
和 next
值。您必须向后续页面发出额外请求才能收集所有结果。
我的模型是:
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": []
}
您在这里使用的是 分页 API。 4
的长度是由于反序列化 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 个结果的测试,这就足够了。但要小心 - 如果您曾经有过希望生成比页面大小(在设置中配置)更多结果的测试,您可能还需要检查 count
和 next
值。您必须向后续页面发出额外请求才能收集所有结果。