Django - 如何对管理 M2M 关系的视图进行单元测试
Django - How to unit test view managing M2M relationship
我有一个实现 m2m 关系的视图,我想对其进行单元测试,但我还没有做到。该视图似乎适用于我定义的页面,但也欢迎任何建议。
上下文如下:我想在 Django 应用程序中管理用户组,当然,由于我需要额外的字段,我在我的应用程序中构建了一个专门用于用户管理的模型。我定义了一个包含多个 select 框的页面,一个用于用户列表,另一个带有用户 selected 成为组的一部分。中间是操作图标,用于将用户从一个组移动到其他组。在这个阶段,如果用户不属于多个组,则无法控制,显示所有不属于当前组的用户(我认为这只是过滤数据的问题)。
我的页面目前看起来像这样(顺便说一句,如果您有任何关于在 select 离子框上方显示标题的建议,我也将不胜感激,即使这不是这里的主题。
我想对每个组的包含进行单元测试,然后再对从组中添加或删除用户的影响进行单元测试。
在这个阶段,我只能检查数据库中显示的用户,但实际上我不知道它是一个组还是另一个组的一部分。如果我添加一些规则,比如验证用户还不是组的一部分(或者在这种情况下不建议将其添加到组中),我需要构建更精确的测试,但我还不知道如何去做。
这是我当前的工作测试代码:
class TestAdmGroups(TestCase):
def setUp(self):
self.company = create_dummy_company("Société de test")
self.group1 = EventGroup.create_group({"company": self.company, "group_name": "Groupe 1", "weight": 40})
self.group2 = EventGroup.create_group({"company": self.company, "group_name": "Groupe 2", "weight": 60})
# self.group2 = EventGroup.objects.create(company=self.company, group_name="Groupe 2", weight=60)
self.user_staff = create_dummy_user(self.company, "staff", group=self.group1, admin=True)
self.usr11 = create_dummy_user(self.company, "user11", group=self.group1)
self.usr12 = create_dummy_user(self.company, "user12", group=self.group1, admin=True)
self.usr13 = create_dummy_user(self.company, "user13")
self.usr14 = create_dummy_user(self.company, "user14")
self.usr21 = create_dummy_user(self.company, "user21", group=self.group2)
self.usr22 = create_dummy_user(self.company, "user22", group=self.group2)
def test_adm_update_group(self):
self.client.force_login(self.user_staff.user)
url = reverse("polls:adm_group_detail", args=[self.company.comp_slug, self.group1.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "user11")
self.assertContains(response, "user14")
self.assertContains(response, "user21")
我想分开结果,并确保 user11
是右列表的一部分,其他值应该是左列表的一部分。
观点如下:
def adm_group_detail(request, comp_slug, grp_id=0):
company = Company.get_company(comp_slug)
if grp_id > 0:
current_group = EventGroup.objects.get(id=grp_id)
group_form = GroupDetail(request.POST or None, instance=current_group)
else:
group_form = GroupDetail(request.POST or None)
group_form.fields['all_users'].queryset = UserComp.objects.\
filter(company=company).\
order_by('user__last_name', 'user__first_name')
if request.method == 'POST':
# Convert the string in a list of user IDs
usr_list = [int(elt) for elt in request.POST['group_list'].split('-') if elt != ""]
group_form.fields['users'].queryset = UserComp.objects.filter(id__in=usr_list).\
order_by('user__last_name', 'user__first_name')
group_form.fields['all_users'].queryset = UserComp.objects.exclude(id__in=usr_list)
if group_form.is_valid():
if grp_id == 0:
# Create empty group
group_data = {
"company": company,
"group_name": group_form.cleaned_data["group_name"],
"weight": group_form.cleaned_data["weight"],
}
new_group = EventGroup.create_group(group_data)
else:
# Update group
new_group = group_form.save()
# Remove all users
group_usr_list = UserComp.objects.filter(eventgroup=new_group)
for usr in group_usr_list:
new_group.users.remove(usr)
# Common part for create and update : add users according to new/updated list
for usr in usr_list:
new_group.users.add(usr)
new_group.save()
# Update form according to latest changes
group_form.fields['all_users'].queryset = UserComp.objects.\
exclude(id__in=usr_list).\
order_by('user__last_name', 'user__first_name')
group_form.fields['group_list'].initial = "-".join([str(elt.id) for elt in new_group.users.all()])
return render(request, "polls/adm_group_detail.html", locals())
我设法使视图在两个列表都属于同一表单的情况下工作,但如果您有任何建议,我可以更改它。
有了这个视图,我注意到我可以通过以下方式从一个列表或另一个列表中获取值:response.context["group_form"]["all_users"]
或 response.context["group_form"]["users"]
但不幸的是,似乎无法输入这些值之一作为 assertContains()
(self.assertContains(response.context["group_form"]["users"], self.user11.user.username)
不起作用,因为第一个参数应该是 response
)也不是 assertInHTML()
,在这种情况下,我有以下错误消息,其中包含相同的先前参数:
======================================================================
ERROR: test_adm_update_group (polls.tests_admin.TestAdmGroups)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\Mes documents\Informatique\Developpement\Votes AG\projet_votes\polls\tests_admin.py", line 264, in test_adm_update_group
self.assertInHTML(response.context["group_form"]["users"], self.usr11.user.username)
File "C:\Users\Christophe\.virtualenvs\projet_votes-onIieQ0I\lib\site-packages\django\test\testcases.py", line 791, in assertInHTML
needle = assert_and_parse_html(self, needle, None, 'First argument is not valid HTML:')
File "C:\Users\Christophe\.virtualenvs\projet_votes-onIieQ0I\lib\site-packages\django\test\testcases.py", line 62, in assert_and_parse_html
dom = parse_html(html)
File "C:\Users\Christophe\.virtualenvs\projet_votes-onIieQ0I\lib\site-packages\django\test\html.py", line 220, in parse_html
parser.feed(html)
File "c:\program files\python37\Lib\html\parser.py", line 110, in feed
self.rawdata = self.rawdata + data
TypeError: can only concatenate str (not "BoundField") to str
----------------------------------------------------------------------
正如您在屏幕截图中看到的,我想检查某个用户是否在一个列表或另一个列表中,而不仅仅是像我那样显示在页面上。
这是模型的定义:
class EventGroup(models.Model):
"""
Groups of users
The link with events is supported by the Event
(as groups can be reused in several Events)
"""
company = models.ForeignKey(
Company, on_delete=models.CASCADE, verbose_name="société"
)
users = models.ManyToManyField(UserComp, verbose_name="utilisateurs", blank=True)
group_name = models.CharField("nom", max_length=100)
weight = models.IntegerField("poids", default=0)
def __str__(self):
return self.group_name
class Meta:
verbose_name = "Groupe d'utilisateurs"
verbose_name_plural = "Groupes d'utilisateurs"
@classmethod
def create_group(cls, group_info):
new_group = EventGroup(company=group_info["company"], group_name=group_info["group_name"], weight=group_info["weight"])
new_group.save()
return new_group
如果有帮助,这里是 HTML 代码:
{% extends './base.html' %}
{% load static %}
{% block content %}
<div class="row">
{% include "./adm_head.html" %}
<div class="col-sm-9">
<input type="hidden" id="menu_id" value="3" /> <!-- Hidden value to store the current selected menu -->
<div class="row">
<div id="admin-groups" class="col-sm-12 text-center">
<h4 class="mt-5">Détails du groupe</h4>
</div>
</div>
<div class="row">
<div class="col-sm-12 mt-30">
{% if grp_id %}
<form action="{% url 'polls:adm_group_detail' company.comp_slug grp_id %}" method="post">
{% else %}
<form action="{% url 'polls:adm_group_detail' company.comp_slug %}" method="post">
{% endif %}
{% csrf_token %}
<div class="row">
<div class="control-group {%if group_form.group_name.errors %}error{%endif%}"></div>
<div class="control-group {%if group_form.weight.errors %}error{%endif%}"></div>
{{ group_form.group_name}} {{ group_form.weight }}
<a type="button" id="disp_detail" class="collapse-group btn btn-sm" href="">
<span id="btn_grp" class="fas fa-chevron-up" data-toggle="tooltip" title="Masquer/Afficher détails"></span>
</a>
</div>
<div class="row mt-30 grp-content" id="grp_content">
<div class="col-md-5 d-flex justify-content-center">
<p>Utilisateurs</p>
{{ group_form.all_users}}
</div>
<div class="col-md-2 d-flex flex-column text-center justify-content-around">
<a type="button" id="add_all" class="update-user btn btn-sm" href="">
<span class="fa fa-fast-forward" style="color: #339af0;" data-toggle="tooltip" title="Ajouter tout"></span>
</a>
<a type="button" id="add_selected" class="update-user btn btn-sm" href="">
<span class="fa fa-step-forward" style="color: #339af0;" data-toggle="tooltip" title="Ajouter sélection"></span>
</a>
<a type="button" id="remove_selected" class="update-user btn btn-sm" href="">
<span class="fa fa-step-backward" style="color: #339af0;" data-toggle="tooltip" title="Retirer sélection"></span>
</a>
<a type="button" id="remove_all" class="update-user btn btn-sm" href="">
<span class="fa fa-fast-backward" style="color: #339af0;" data-toggle="tooltip" title="Retirer tout"></span>
</a>
</div>
<div class="col-md-5 d-flex justify-content-center">
<p>Utilisateurs sélectionnés</p><br>
{{ group_form.users }}
<div class="control-group {%if group_form.users.errors %}error{%endif%}"></div>
</div>
</div>
<div class="row">
<div class="col-sm-12 mt-30 text-center">
<button id='upd_grp' class="btn btn-success" type="submit">{% if grp_id %}Mettre à jour{% else %}Créer{% endif %}</button>
     
<a class="btn btn-secondary back_btn" href="*">Annuler</a>
</div>
</div>
<div class="row">
<div hidden>
<!-- Liste des ID du groupe -->
{{ group_form.group_list }}
</div>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
我想我找到了我一直在寻找的答案和进行测试的方法。
想法是找到一种方法来分离每个表单的包含,这要归功于属性的正确应用:将主表单隔离为 context
dict 的键,然后使用 fields
属性来过滤并最终应用 queryset
属性以便能够相应地管理相关数据。
然后,问题是:'how to make the comparison with this specific format?'。我通过过滤这个对象找到了答案,利用 .filter()
如果没有找到值将检索一个空列表,而 .get()
会引发错误。
在我完成之前我不会做出正确的回答,或者我收到一些评论或其他想法以获得更好的解决方案,但以下单元测试代码对我来说非常有效:
class TestAdmGroups(TestCase):
def setUp(self):
self.company = create_dummy_company("Société de test")
self.group1 = EventGroup.create_group({"company": self.company, "group_name": "Groupe 1", "weight": 40})
self.group2 = EventGroup.create_group({"company": self.company, "group_name": "Groupe 2", "weight": 60})
# self.group2 = EventGroup.objects.create(company=self.company, group_name="Groupe 2", weight=60)
self.user_staff = create_dummy_user(self.company, "staff", group=self.group1, admin=True)
self.usr11 = create_dummy_user(self.company, "user11", group=self.group1)
self.usr12 = create_dummy_user(self.company, "user12", group=self.group1, admin=True)
self.usr13 = create_dummy_user(self.company, "user13")
self.usr14 = create_dummy_user(self.company, "user14")
self.usr21 = create_dummy_user(self.company, "user21", group=self.group2)
self.usr22 = create_dummy_user(self.company, "user22", group=self.group2)
def test_adm_update_group(self):
self.client.force_login(self.user_staff.user)
url = reverse("polls:adm_group_detail", args=[self.company.comp_slug, self.group1.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
group_users = response.context["group_form"].fields["users"].queryset
test_user = group_users.filter(id=self.usr11.id)
self.assertEqual(len(test_user), 1)
test_user = group_users.filter(id=self.usr14.id)
self.assertEqual(len(test_user), 0)
我有一个实现 m2m 关系的视图,我想对其进行单元测试,但我还没有做到。该视图似乎适用于我定义的页面,但也欢迎任何建议。
上下文如下:我想在 Django 应用程序中管理用户组,当然,由于我需要额外的字段,我在我的应用程序中构建了一个专门用于用户管理的模型。我定义了一个包含多个 select 框的页面,一个用于用户列表,另一个带有用户 selected 成为组的一部分。中间是操作图标,用于将用户从一个组移动到其他组。在这个阶段,如果用户不属于多个组,则无法控制,显示所有不属于当前组的用户(我认为这只是过滤数据的问题)。
我的页面目前看起来像这样(顺便说一句,如果您有任何关于在 select 离子框上方显示标题的建议,我也将不胜感激,即使这不是这里的主题。
我想对每个组的包含进行单元测试,然后再对从组中添加或删除用户的影响进行单元测试。
在这个阶段,我只能检查数据库中显示的用户,但实际上我不知道它是一个组还是另一个组的一部分。如果我添加一些规则,比如验证用户还不是组的一部分(或者在这种情况下不建议将其添加到组中),我需要构建更精确的测试,但我还不知道如何去做。
这是我当前的工作测试代码:
class TestAdmGroups(TestCase):
def setUp(self):
self.company = create_dummy_company("Société de test")
self.group1 = EventGroup.create_group({"company": self.company, "group_name": "Groupe 1", "weight": 40})
self.group2 = EventGroup.create_group({"company": self.company, "group_name": "Groupe 2", "weight": 60})
# self.group2 = EventGroup.objects.create(company=self.company, group_name="Groupe 2", weight=60)
self.user_staff = create_dummy_user(self.company, "staff", group=self.group1, admin=True)
self.usr11 = create_dummy_user(self.company, "user11", group=self.group1)
self.usr12 = create_dummy_user(self.company, "user12", group=self.group1, admin=True)
self.usr13 = create_dummy_user(self.company, "user13")
self.usr14 = create_dummy_user(self.company, "user14")
self.usr21 = create_dummy_user(self.company, "user21", group=self.group2)
self.usr22 = create_dummy_user(self.company, "user22", group=self.group2)
def test_adm_update_group(self):
self.client.force_login(self.user_staff.user)
url = reverse("polls:adm_group_detail", args=[self.company.comp_slug, self.group1.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "user11")
self.assertContains(response, "user14")
self.assertContains(response, "user21")
我想分开结果,并确保 user11
是右列表的一部分,其他值应该是左列表的一部分。
观点如下:
def adm_group_detail(request, comp_slug, grp_id=0):
company = Company.get_company(comp_slug)
if grp_id > 0:
current_group = EventGroup.objects.get(id=grp_id)
group_form = GroupDetail(request.POST or None, instance=current_group)
else:
group_form = GroupDetail(request.POST or None)
group_form.fields['all_users'].queryset = UserComp.objects.\
filter(company=company).\
order_by('user__last_name', 'user__first_name')
if request.method == 'POST':
# Convert the string in a list of user IDs
usr_list = [int(elt) for elt in request.POST['group_list'].split('-') if elt != ""]
group_form.fields['users'].queryset = UserComp.objects.filter(id__in=usr_list).\
order_by('user__last_name', 'user__first_name')
group_form.fields['all_users'].queryset = UserComp.objects.exclude(id__in=usr_list)
if group_form.is_valid():
if grp_id == 0:
# Create empty group
group_data = {
"company": company,
"group_name": group_form.cleaned_data["group_name"],
"weight": group_form.cleaned_data["weight"],
}
new_group = EventGroup.create_group(group_data)
else:
# Update group
new_group = group_form.save()
# Remove all users
group_usr_list = UserComp.objects.filter(eventgroup=new_group)
for usr in group_usr_list:
new_group.users.remove(usr)
# Common part for create and update : add users according to new/updated list
for usr in usr_list:
new_group.users.add(usr)
new_group.save()
# Update form according to latest changes
group_form.fields['all_users'].queryset = UserComp.objects.\
exclude(id__in=usr_list).\
order_by('user__last_name', 'user__first_name')
group_form.fields['group_list'].initial = "-".join([str(elt.id) for elt in new_group.users.all()])
return render(request, "polls/adm_group_detail.html", locals())
我设法使视图在两个列表都属于同一表单的情况下工作,但如果您有任何建议,我可以更改它。
有了这个视图,我注意到我可以通过以下方式从一个列表或另一个列表中获取值:response.context["group_form"]["all_users"]
或 response.context["group_form"]["users"]
但不幸的是,似乎无法输入这些值之一作为 assertContains()
(self.assertContains(response.context["group_form"]["users"], self.user11.user.username)
不起作用,因为第一个参数应该是 response
)也不是 assertInHTML()
,在这种情况下,我有以下错误消息,其中包含相同的先前参数:
======================================================================
ERROR: test_adm_update_group (polls.tests_admin.TestAdmGroups)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\Mes documents\Informatique\Developpement\Votes AG\projet_votes\polls\tests_admin.py", line 264, in test_adm_update_group
self.assertInHTML(response.context["group_form"]["users"], self.usr11.user.username)
File "C:\Users\Christophe\.virtualenvs\projet_votes-onIieQ0I\lib\site-packages\django\test\testcases.py", line 791, in assertInHTML
needle = assert_and_parse_html(self, needle, None, 'First argument is not valid HTML:')
File "C:\Users\Christophe\.virtualenvs\projet_votes-onIieQ0I\lib\site-packages\django\test\testcases.py", line 62, in assert_and_parse_html
dom = parse_html(html)
File "C:\Users\Christophe\.virtualenvs\projet_votes-onIieQ0I\lib\site-packages\django\test\html.py", line 220, in parse_html
parser.feed(html)
File "c:\program files\python37\Lib\html\parser.py", line 110, in feed
self.rawdata = self.rawdata + data
TypeError: can only concatenate str (not "BoundField") to str
----------------------------------------------------------------------
正如您在屏幕截图中看到的,我想检查某个用户是否在一个列表或另一个列表中,而不仅仅是像我那样显示在页面上。
这是模型的定义:
class EventGroup(models.Model):
"""
Groups of users
The link with events is supported by the Event
(as groups can be reused in several Events)
"""
company = models.ForeignKey(
Company, on_delete=models.CASCADE, verbose_name="société"
)
users = models.ManyToManyField(UserComp, verbose_name="utilisateurs", blank=True)
group_name = models.CharField("nom", max_length=100)
weight = models.IntegerField("poids", default=0)
def __str__(self):
return self.group_name
class Meta:
verbose_name = "Groupe d'utilisateurs"
verbose_name_plural = "Groupes d'utilisateurs"
@classmethod
def create_group(cls, group_info):
new_group = EventGroup(company=group_info["company"], group_name=group_info["group_name"], weight=group_info["weight"])
new_group.save()
return new_group
如果有帮助,这里是 HTML 代码:
{% extends './base.html' %}
{% load static %}
{% block content %}
<div class="row">
{% include "./adm_head.html" %}
<div class="col-sm-9">
<input type="hidden" id="menu_id" value="3" /> <!-- Hidden value to store the current selected menu -->
<div class="row">
<div id="admin-groups" class="col-sm-12 text-center">
<h4 class="mt-5">Détails du groupe</h4>
</div>
</div>
<div class="row">
<div class="col-sm-12 mt-30">
{% if grp_id %}
<form action="{% url 'polls:adm_group_detail' company.comp_slug grp_id %}" method="post">
{% else %}
<form action="{% url 'polls:adm_group_detail' company.comp_slug %}" method="post">
{% endif %}
{% csrf_token %}
<div class="row">
<div class="control-group {%if group_form.group_name.errors %}error{%endif%}"></div>
<div class="control-group {%if group_form.weight.errors %}error{%endif%}"></div>
{{ group_form.group_name}} {{ group_form.weight }}
<a type="button" id="disp_detail" class="collapse-group btn btn-sm" href="">
<span id="btn_grp" class="fas fa-chevron-up" data-toggle="tooltip" title="Masquer/Afficher détails"></span>
</a>
</div>
<div class="row mt-30 grp-content" id="grp_content">
<div class="col-md-5 d-flex justify-content-center">
<p>Utilisateurs</p>
{{ group_form.all_users}}
</div>
<div class="col-md-2 d-flex flex-column text-center justify-content-around">
<a type="button" id="add_all" class="update-user btn btn-sm" href="">
<span class="fa fa-fast-forward" style="color: #339af0;" data-toggle="tooltip" title="Ajouter tout"></span>
</a>
<a type="button" id="add_selected" class="update-user btn btn-sm" href="">
<span class="fa fa-step-forward" style="color: #339af0;" data-toggle="tooltip" title="Ajouter sélection"></span>
</a>
<a type="button" id="remove_selected" class="update-user btn btn-sm" href="">
<span class="fa fa-step-backward" style="color: #339af0;" data-toggle="tooltip" title="Retirer sélection"></span>
</a>
<a type="button" id="remove_all" class="update-user btn btn-sm" href="">
<span class="fa fa-fast-backward" style="color: #339af0;" data-toggle="tooltip" title="Retirer tout"></span>
</a>
</div>
<div class="col-md-5 d-flex justify-content-center">
<p>Utilisateurs sélectionnés</p><br>
{{ group_form.users }}
<div class="control-group {%if group_form.users.errors %}error{%endif%}"></div>
</div>
</div>
<div class="row">
<div class="col-sm-12 mt-30 text-center">
<button id='upd_grp' class="btn btn-success" type="submit">{% if grp_id %}Mettre à jour{% else %}Créer{% endif %}</button>
     
<a class="btn btn-secondary back_btn" href="*">Annuler</a>
</div>
</div>
<div class="row">
<div hidden>
<!-- Liste des ID du groupe -->
{{ group_form.group_list }}
</div>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
我想我找到了我一直在寻找的答案和进行测试的方法。
想法是找到一种方法来分离每个表单的包含,这要归功于属性的正确应用:将主表单隔离为 context
dict 的键,然后使用 fields
属性来过滤并最终应用 queryset
属性以便能够相应地管理相关数据。
然后,问题是:'how to make the comparison with this specific format?'。我通过过滤这个对象找到了答案,利用 .filter()
如果没有找到值将检索一个空列表,而 .get()
会引发错误。
在我完成之前我不会做出正确的回答,或者我收到一些评论或其他想法以获得更好的解决方案,但以下单元测试代码对我来说非常有效:
class TestAdmGroups(TestCase):
def setUp(self):
self.company = create_dummy_company("Société de test")
self.group1 = EventGroup.create_group({"company": self.company, "group_name": "Groupe 1", "weight": 40})
self.group2 = EventGroup.create_group({"company": self.company, "group_name": "Groupe 2", "weight": 60})
# self.group2 = EventGroup.objects.create(company=self.company, group_name="Groupe 2", weight=60)
self.user_staff = create_dummy_user(self.company, "staff", group=self.group1, admin=True)
self.usr11 = create_dummy_user(self.company, "user11", group=self.group1)
self.usr12 = create_dummy_user(self.company, "user12", group=self.group1, admin=True)
self.usr13 = create_dummy_user(self.company, "user13")
self.usr14 = create_dummy_user(self.company, "user14")
self.usr21 = create_dummy_user(self.company, "user21", group=self.group2)
self.usr22 = create_dummy_user(self.company, "user22", group=self.group2)
def test_adm_update_group(self):
self.client.force_login(self.user_staff.user)
url = reverse("polls:adm_group_detail", args=[self.company.comp_slug, self.group1.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
group_users = response.context["group_form"].fields["users"].queryset
test_user = group_users.filter(id=self.usr11.id)
self.assertEqual(len(test_user), 1)
test_user = group_users.filter(id=self.usr14.id)
self.assertEqual(len(test_user), 0)