Django — Как выполнить модульное тестирование для управления отношениями M2M

#django #unit-testing #many-to-many #multiple-select

Вопрос:

У меня есть представление, реализующее отношения m2m, и я хотел бы провести его модульное тестирование, которое мне еще не удалось. Представление, похоже, работает со страницей, которую я определил, но любое предложение также приветствуется.
Контекст следующий: я хотел бы управлять группами пользователей в приложении Django, и, конечно, поскольку мне нужны дополнительные поля, я создал модель, посвященную управлению пользователями в моем приложении. Я определил страницу с несколькими полями выбора, одно из которых предназначено для списка пользователей, а другое-для пользователей, выбранных для включения в группу. Между ними находятся значки действий для перемещения пользователей из одной группы в другие. На данном этапе нет контроля, если пользователи не должны принадлежать более чем к одной группе, отображаются все пользователи, которые не принадлежат к текущей группе (я предполагаю, что это просто вопрос фильтрации данных).
Моя страница в настоящее время выглядит так (кстати, если у вас есть какие-либо предложения по отображению заголовков над полями выбора, я также был бы признателен, даже если это не тема здесь.
введите описание изображения здесь

Я хотел бы модульно проверить содержимое каждой группы и, позже, влияние добавления или удаления пользователя из группы.
На этом этапе я просто смог проверить, отображается ли пользователь в базе данных, но на самом деле я понятия не имею, входит ли он в ту или иную группу. Если я добавлю некоторые правила, такие как проверка того, что пользователь еще не входит в группу (или в этом случае не предлагайте добавлять его в группу), мне нужно построить более точный тест, и я еще не знаю, как это сделать.

Вот мой текущий рабочий тестовый код:

 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")  

I would like to separate results, and ensure user11 is part of the right list, the other values are supposed to be part of the left one.

The view is the following:

 def adm_group_detail(request, comp_slug, grp_id=0):  company = Company.get_company(comp_slug)  if grp_id gt; 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())  

I managed to make the view work with both lists being part of the same form, but I can change this if you have any suggestion.
With this view, I noticed that I could get values from one list or another the following way: response.context["group_form"]["all_users"] or response.context["group_form"]["users"] but unfortunately, it looks like it’s not possible to enter one of these values as parameter of assertContains() ( self.assertContains(response.context["group_form"]["users"], self.user11.user.username) does not work, as the fist parameter is supposed to be a response ) nor assertInHTML() , in this case I have the following error message with the same previous parameters:

 ====================================================================== ERROR: test_adm_update_group (polls.tests_admin.TestAdmGroups) ---------------------------------------------------------------------- Traceback (most recent call last):  File "D:Mes documentsInformatiqueDeveloppementVotes AGprojet_votespollstests_admin.py", line 264, in test_adm_update_group  self.assertInHTML(response.context["group_form"]["users"], self.usr11.user.username)  File "C:UsersChristophe.virtualenvsprojet_votes-onIieQ0Ilibsite-packagesdjangotesttestcases.py", line 791, in assertInHTML   needle = assert_and_parse_html(self, needle, None, 'First argument is not valid HTML:')  File "C:UsersChristophe.virtualenvsprojet_votes-onIieQ0Ilibsite-packagesdjangotesttestcases.py", line 62, in assert_and_parse_html  dom = parse_html(html)  File "C:UsersChristophe.virtualenvsprojet_votes-onIieQ0Ilibsite-packagesdjangotesthtml.py", line 220, in parse_html  parser.feed(html)  File "c:program filespython37Libhtmlparser.py", line 110, in feed  self.rawdata = self.rawdata   data TypeError: can only concatenate str (not "BoundField") to str  ----------------------------------------------------------------------  

As you can see in the screenshot, I would like to check a user is in one list or another, not only displayed on the page like I did yet.

Here is the model’s definition:

 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  

If it can help, here is the HTML code:

 {% extends './base.html' %}  {% load static %}  {% block content %}  lt;div class="row"gt;  {% include "./adm_head.html" %}   lt;div class="col-sm-9"gt;  lt;input type="hidden" id="menu_id" value="3" /gt; lt;!-- Hidden value to store the current selected menu --gt;  lt;div class="row"gt;  lt;div id="admin-groups" class="col-sm-12 text-center"gt;  lt;h4 class="mt-5"gt;Détails du groupelt;/h4gt;  lt;/divgt;  lt;/divgt;   lt;div class="row"gt;  lt;div class="col-sm-12 mt-30"gt;  {% if grp_id %}  lt;form action="{% url 'polls:adm_group_detail' company.comp_slug grp_id %}" method="post"gt;  {% else %}  lt;form action="{% url 'polls:adm_group_detail' company.comp_slug %}" method="post"gt;  {% endif %}  {% csrf_token %}  lt;div class="row"gt;  lt;div class="control-group {%if group_form.group_name.errors %}error{%endif%}"gt;lt;/divgt;  lt;div class="control-group {%if group_form.weight.errors %}error{%endif%}"gt;lt;/divgt;  {{ group_form.group_name}} {{ group_form.weight }}  lt;a type="button" id="disp_detail" class="collapse-group btn btn-sm" href=""gt;  lt;span id="btn_grp" class="fas fa-chevron-up" data-toggle="tooltip" title="Masquer/Afficher détails"gt;lt;/spangt;  lt;/agt;  lt;/divgt;  lt;div class="row mt-30 grp-content" id="grp_content"gt;  lt;div class="col-md-5 d-flex justify-content-center"gt;  lt;pgt;Utilisateurslt;/pgt;  {{ group_form.all_users}}  lt;/divgt;  lt;div class="col-md-2 d-flex flex-column text-center justify-content-around"gt;  lt;a type="button" id="add_all" class="update-user btn btn-sm" href=""gt;  lt;span class="fa fa-fast-forward" style="color: #339af0;" data-toggle="tooltip" title="Ajouter tout"gt;lt;/spangt;  lt;/agt;  lt;a type="button" id="add_selected" class="update-user btn btn-sm" href=""gt;  lt;span class="fa fa-step-forward" style="color: #339af0;" data-toggle="tooltip" title="Ajouter sélection"gt;lt;/spangt;  lt;/agt;  lt;a type="button" id="remove_selected" class="update-user btn btn-sm" href=""gt;  lt;span class="fa fa-step-backward" style="color: #339af0;" data-toggle="tooltip" title="Retirer sélection"gt;lt;/spangt;  lt;/agt;  lt;a type="button" id="remove_all" class="update-user btn btn-sm" href=""gt;  lt;span class="fa fa-fast-backward" style="color: #339af0;" data-toggle="tooltip" title="Retirer tout"gt;lt;/spangt;  lt;/agt;  lt;/divgt;  lt;div class="col-md-5 d-flex justify-content-center"gt;  lt;pgt;Utilisateurs sélectionnéslt;/pgt;lt;brgt;  {{ group_form.users }}  lt;div class="control-group {%if group_form.users.errors %}error{%endif%}"gt;lt;/divgt;  lt;/divgt;  lt;/divgt;  lt;div class="row"gt;  lt;div class="col-sm-12 mt-30 text-center"gt;  lt;button id='upd_grp' class="btn btn-success" type="submit"gt;{% if grp_id %}Mettre à jour{% else %}Créer{% endif %}lt;/buttongt;  amp;nbsp amp;nbsp amp;nbsp  lt;a class="btn btn-secondary back_btn" href="*"gt;Annulerlt;/agt;  lt;/divgt;  lt;/divgt;  lt;div class="row"gt;  lt;div hiddengt;  lt;!-- Liste des ID du groupe --gt;  {{ group_form.group_list }}  lt;/divgt;  lt;/divgt;  lt;/formgt;  lt;/divgt;  lt;/divgt;   lt;/divgt;  {% endblock %}  

Ответ №1:

Я думаю, что нашел ответ, который искал, и способ провести свои тесты.
Идея состояла в том, чтобы найти способ разделить содержимое каждой формы, и это сделано благодаря правильному применению атрибутов: изолировать основную форму в качестве ключа context dict, затем использовать fields атрибут для фильтрации и, наконец, применить queryset атрибут, чтобы иметь возможность соответствующим образом управлять связанными данными.
Затем возник вопрос: «как провести сравнение с этим конкретным форматом?». Я нашел ответ, отфильтровав этот объект, воспользовавшись тем, что .filter() он получит пустой список, если значение не найдено, в то время как a .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)