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>
                            &nbsp &nbsp &nbsp
                            <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)