Django 3.0.8 测试用例失败

Posted

技术标签:

【中文标题】Django 3.0.8 测试用例失败【英文标题】:Django 3.0.8 Test Cases Failing 【发布时间】:2020-12-15 01:29:36 【问题描述】:

我正在尝试遵循Simple is Better than Complex 教程。具体来说,我刚刚完成了Part 4。我在运行测试时遇到以下 4 种失败:

$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.............................FFF.F..................................
======================================================================
FAIL: test_contains_form (accounts.tests.test_view_password_reset.PasswordResetTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\admin\Desktop\Development\Django\myproject\myproject\accounts\tests\tes
t_view_password_reset.py", line 30, in test_contains_form
    self.assertIsInstance(form, PasswordResetForm)
AssertionError: None is not an instance of <class 'django.contrib.auth.forms.PasswordRes
etForm'>

======================================================================
FAIL: test_csrf (accounts.tests.test_view_password_reset.PasswordResetTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\admin\Desktop\Development\Django\myproject\myproject\accounts\tests\tes
t_view_password_reset.py", line 26, in test_csrf
    self.assertContains(self.response, 'csrfmiddlewaretoken')
  File "C:\Users\admin\Desktop\Development\Django\myproject\venv\lib\site-packages\djang
o\test\testcases.py", line 454, in assertContains
    self.assertTrue(real_count != 0, msg_prefix + "Couldn't find %s in response" % text_
repr)
AssertionError: False is not true : Couldn't find 'csrfmiddlewaretoken' in response

======================================================================
FAIL: test_form_inputs (accounts.tests.test_view_password_reset.PasswordResetTests)
The view must contain two inputs: csrf and email
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\admin\Desktop\Development\Django\myproject\myproject\accounts\tests\tes
t_view_password_reset.py", line 36, in test_form_inputs
    self.assertContains(self.response, '<input', 2)
  File "C:\Users\admin\Desktop\Development\Django\myproject\venv\lib\site-packages\djang
o\test\testcases.py", line 449, in assertContains
    self.assertEqual(
AssertionError: 0 != 2 : Found 0 instances of '<input' in response (expected 2)

======================================================================
FAIL: test_view_function (accounts.tests.test_view_password_reset.PasswordResetTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\admin\Desktop\Development\Django\myproject\myproject\accounts\tests\tes
t_view_password_reset.py", line 23, in test_view_function
    self.assertEquals(view.func.view_class, auth_views.PasswordResetView)
AssertionError: <class 'django.contrib.auth.views.PasswordResetDoneView'> != <class 'dja
ngo.contrib.auth.views.PasswordResetView'>

----------------------------------------------------------------------
Ran 68 tests in 9.235s

FAILED (failures=4)
Destroying test database for alias 'default'...

这是我的文件结构:

├───myproject
│   │   db.sqlite3
│   │   manage.py
│   │   
│   ├───accounts
│   │   │   admin.py
│   │   │   apps.py
│   │   │   forms.py
│   │   │   models.py
│   │   │   views.py
│   │   │   __init__.py
│   │   │   
│   │   ├───migrations
│   │   │   │   __init__.py
│   │   │   │   
│   │   │   └───__pycache__
│   │   │           
│   │   ├───tests
│   │   │   │   test_form_signup.py
│   │   │   │   test_mail_password_reset.py
│   │   │   │   test_view_password_change.py
│   │   │   │   test_view_password_reset.py
│   │   │   │   test_view_signup.py
│   │   │   │   __init__.py
│   │   │   │   
│   │   │   └───__pycache__
│   │   │           
│   │   └───__pycache__
│   │           
│   ├───boards
│   │   │   admin.py
│   │   │   apps.py
│   │   │   forms.py
│   │   │   models.py
│   │   │   views.py
│   │   │   __init__.py
│   │   │   
│   │   ├───migrations
│   │   │   │   0001_initial.py
│   │   │   │   __init__.py
│   │   │   │   
│   │   │   └───__pycache__
│   │   │           
│   │   ├───templatetags
│   │   │   │   form_tags.py
│   │   │   │   __init__.py
│   │   │   │   
│   │   │   └───__pycache__
│   │   │           
│   │   ├───tests
│   │   │   │   test_templatetags.py
│   │   │   │   test_views.py
│   │   │   │   __init__.py
│   │   │   │   
│   │   │   └───__pycache__
│   │   │           
│   │   └───__pycache__
│   │           
│   ├───myproject
│   │   │   asgi.py
│   │   │   settings.py
│   │   │   urls.py
│   │   │   wsgi.py
│   │   │   __init__.py
│   │   │   
│   │   └───__pycache__
│   │           
│   ├───static
│   │   ├───css
│   │   │       accounts.css
│   │   │       app.css
│   │   │       bootstrap.min.css
│   │   │       
│   │   ├───img
│   │   │       moroccan-flower-dark.png
│   │   │       y-so-serious.png
│   │   │       
│   │   ├───js
│   │   │       bootstrap.min.js
│   │   │       jquery-3.5.1.min.js
│   │   │       popper.min.js
│   │           
│   └───templates
│       │   base.html
│       │   base_accounts.html
│       │   home.html
│       │   login.html
│       │   new_topic.html
│       │   password_change.html
│       │   password_change_done.html
│       │   password_reset.html
│       │   password_reset_complete.html
│       │   password_reset_confirm.html
│       │   password_reset_done.html
│       │   password_reset_email.html
│       │   password_reset_subject.txt
│       │   signup.html
│       │   topics.html
│       │   
│       └───includes
│               form.html

这是myproject\accounts\tests\test_view_password_reset.py 中似乎引发错误的部分:

from django.contrib.auth.tokens import default_token_generator
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.contrib.auth import views as auth_views
from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm
from django.contrib.auth.models import User
from django.core import mail
from django.urls import reverse
from django.urls import resolve
from django.test import TestCase


class PasswordResetTests(TestCase):
    def setUp(self):
        url = reverse('password_reset_done')
        self.response = self.client.get(url)

    def test_status_code(self):
        self.assertEquals(self.response.status_code, 200)

    def test_view_function(self):
        view = resolve('/reset/done/')
        self.assertEquals(view.func.view_class, auth_views.PasswordResetView)

    def test_csrf(self):
        self.assertContains(self.response, 'csrfmiddlewaretoken')

    def test_contains_form(self):
        form = self.response.context.get('form')
        self.assertIsInstance(form, PasswordResetForm)

    def test_form_inputs(self):
        '''
        The view must contain two inputs: csrf and email
        '''
        self.assertContains(self.response, '<input', 2)
        self.assertContains(self.response, 'type="email"', 1)


class SuccessfulPasswordResetTests(TestCase):
    def setUp(self):
        email = 'john@doe.com'
        User.objects.create_user(username='john', email=email, password='123abcdef')
        url = reverse('password_reset')
        self.response = self.client.post(url, 'email': email)

    def test_redirection(self):
        '''
        A valid form submission should redirect the user to `password_reset_done` view
        '''
        url = reverse('password_reset_done')
        self.assertRedirects(self.response, url)

    def test_send_password_reset_email(self):
        self.assertEqual(1, len(mail.outbox))


class InvalidPasswordResetTests(TestCase):
    def setUp(self):
        url = reverse('password_reset')
        self.response = self.client.post(url, 'email': 'donotexist@email.com')

    def test_redirection(self):
        '''
        Even invalid emails in the database should
        redirect the user to `password_reset_done` view
        '''
        url = reverse('password_reset_done')
        self.assertRedirects(self.response, url)

    def test_no_reset_email_sent(self):
        self.assertEqual(0, len(mail.outbox))

class PasswordResetDoneTests(TestCase):
    def setUp(self):
        url = reverse('password_reset_done')
        self.response = self.client.get(url)

    def test_status_code(self):
        self.assertEquals(self.response.status_code, 200)

    def test_view_function(self):
        view = resolve('/reset/done/')
        self.assertEquals(view.func.view_class, auth_views.PasswordResetDoneView)

class PasswordResetConfirmTests(TestCase):
    def setUp(self):
        user = User.objects.create_user(username='john', email='john@doe.com', password='123abcdef')

        '''
        create a valid password reset token
        based on how django creates the token internally:
        https://github.com/django/django/blob/1.11.5/django/contrib/auth/forms.py#L280
        '''
        self.uid = urlsafe_base64_encode(force_bytes(user.pk))
        self.token = default_token_generator.make_token(user)

        url = reverse('password_reset_confirm', kwargs='uidb64': self.uid, 'token': self.token)
        self.response = self.client.get(url, follow=True)

    def test_status_code(self):
        self.assertEquals(self.response.status_code, 200)

    def test_view_function(self):
        view = resolve('/reset/uidb64/token/'.format(uidb64=self.uid, token=self.token))
        self.assertEquals(view.func.view_class, auth_views.PasswordResetConfirmView)

    def test_csrf(self):
        self.assertContains(self.response, 'csrfmiddlewaretoken')

    def test_contains_form(self):
        form = self.response.context.get('form')
        self.assertIsInstance(form, SetPasswordForm)

    def test_form_inputs(self):
        '''
        The view must contain two inputs: csrf and two password fields
        '''
        self.assertContains(self.response, '<input', 3)
        self.assertContains(self.response, 'type="password"', 2)


class InvalidPasswordResetConfirmTests(TestCase):
    def setUp(self):
        user = User.objects.create_user(username='john', email='john@doe.com', password='123abcdef')
        uid = urlsafe_base64_encode(force_bytes(user.pk))
        token = default_token_generator.make_token(user)

        '''
        invalidate the token by changing the password
        '''
        user.set_password('abcdef123')
        user.save()

        url = reverse('password_reset_confirm', kwargs='uidb64': uid, 'token': token)
        self.response = self.client.get(url)

    def test_status_code(self):
        self.assertEquals(self.response.status_code, 200)

    def test_html(self):
        password_reset_url = reverse('password_reset')
        self.assertContains(self.response, 'invalid password reset link')
        self.assertContains(self.response, 'href="0"'.format(password_reset_url))

class PasswordResetCompleteTests(TestCase):
    def setUp(self):
        url = reverse('password_reset_complete')
        self.response = self.client.get(url)

    def test_status_code(self):
        self.assertEquals(self.response.status_code, 200)

    def test_view_function(self):
        view = resolve('/reset/complete/')
        self.assertEquals(view.func.view_class, auth_views.PasswordResetCompleteView)

我的项目\urls.py

from django.contrib import admin
from django.urls import path
from django.contrib.auth import views as auth_views

from accounts import views as accounts_views
from boards import views
from django.conf.urls import url

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^signup/$', accounts_views.signup, name='signup'),
    url(r'^login/$', auth_views.LoginView.as_view(template_name='login.html'), name='login'),
    url(r'^logout/$', auth_views.LogoutView.as_view(), name='logout'),
    url(r'^boards/(?P<pk>\d+)/$', views.board_topics, name='board_topics'),
    url(r'^boards/(?P<pk>\d+)/new/$', views.new_topic, name='new_topic'),
    path('admin/', admin.site.urls),

    #Password Reset URLs
    url(r'^reset/$',
        auth_views.PasswordResetView.as_view(
            template_name='password_reset.html',
            email_template_name='password_reset_email.html',
            subject_template_name='password_reset_subject.txt'
        ),
        name='password_reset'),
    url(r'^reset/done/$',
        auth_views.PasswordResetDoneView.as_view(template_name='password_reset_done.html'),
        name='password_reset_done'),
    url(r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]1,13-[0-9A-Za-z]1,20)/$',
        auth_views.PasswordResetConfirmView.as_view(template_name='password_reset_confirm.html'),
        name='password_reset_confirm'),
    url(r'^reset/complete/$',
        auth_views.PasswordResetCompleteView.as_view(template_name='password_reset_complete.html'),
        name='password_reset_complete'),

    url(r'^settings/password/$', auth_views.PasswordChangeView.as_view(template_name='password_change.html'),
        name='password_change'),
    url(r'^settings/password/done/$', auth_views.PasswordChangeDoneView.as_view(template_name='password_change_done.html'),
        name='password_change_done'),
]

accounts\views.py

from django.contrib.auth import login as auth_login
from django.shortcuts import render, redirect

from .forms import SignUpForm

# Create your views here.
def signup(request):
    if request.method == 'POST':
        form = SignUpForm(request.POST)
        if form.is_valid():
            user = form.save()
            auth_login(request, user)
            return redirect('home')
    else:
        form = SignUpForm()
    return render(request, 'signup.html', 'form': form)

boards\views.py

from django.shortcuts import render
from django.http import HttpResponse
from django.http import Http404
from django.contrib.auth.models import User
from django.shortcuts import render, redirect, get_object_or_404
from .forms import NewTopicForm
from .models import Board, Topic, Post

# Create your views here.
def home(request):
    boards = Board.objects.all()
    return render(request, 'home.html', 'boards': boards)

def board_topics(request, pk):
    board = get_object_or_404(Board, pk=pk)
    return render(request, 'topics.html', 'board': board)

def new_topic(request, pk):
    board = get_object_or_404(Board, pk=pk)
    user = User.objects.first()  # TODO: get the currently logged in user
    if request.method == 'POST':
        form = NewTopicForm(request.POST)
        if form.is_valid():
            topic = form.save(commit=False)
            topic.board = board
            topic.starter = user
            topic.save()
            post = Post.objects.create(
                message=form.cleaned_data.get('message'),
                topic=topic,
                created_by=user
            )
            return redirect('board_topics', pk=board.pk)  # TODO: redirect to the created topic page
    else:
        form = NewTopicForm()
    return render(request, 'new_topic.html', 'board': board, 'form': form)

模板\base.html

% load static %<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>% block title %Django Boards% endblock %</title>
    <link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="% static 'css/bootstrap.min.css' %">
    <link rel="stylesheet" href="% static 'css/app.css' %">
    % block stylesheet %% endblock %
</head>
<body>
    % block body %
        <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
            <div class="container">

                <a class="navbar-brand" href="% url 'home' %">Django Boards</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#mainMenu" aria-controls="mainMenu" aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>

                <div class="collapse navbar-collapse" id="mainMenu">
                    % if user.is_authenticated %
                        <ul class="navbar-nav ml-auto">
                            <li class="nav-item dropdown">
                                <a class="nav-link dropdown-toggle" href="#" id="userMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                                     user.username 
                                </a>
                                <div class="dropdown-menu dropdown-menu-right" aria-labelledby="userMenu">
                                    <a class="dropdown-item" href="#">My account</a>
                                    <a class="dropdown-item" href="#">Change password</a>
                                    <div class="dropdown-divider"></div>
                                    <a class="dropdown-item" href="% url 'logout' %">Log out</a>
                                </div>
                            </li>
                        </ul>
                    % else %
                        <form class="form-inline ml-auto">
                            <a href="% url 'login' %" class="btn btn-outline-secondary">Log in</a>
                            <a href="% url 'signup' %" class="btn btn-primary ml-2">Sign up</a>
                        </form>
                    % endif %
                </div>

            </div>
        </nav>

        <div class="container">
            <ol class="breadcrumb my-4">
                % block breadcrumb %
                % endblock %
            </ol>
            % block content %
            % endblock %
        </div>
    % endblock body %

    <script src="% static 'js/jquery-3.5.1.min.js' %"></script>
    <script src="% static 'js/popper.min.js' %"></script>
    <script src="% static 'js/bootstrap.min.js' %"></script>

</body>
</html>

模板\base_accounts.html

% extends 'base.html' %

% load static %

% block stylesheet %
    <link rel="stylesheet" href="% static 'css/accounts.css' %">
% endblock %

% block body %
    <div class="container">

        <h1 class="text-center logo my-4">
            <a href="% url 'home' %">Django Boards</a>
        </h1>
        
        % block content %
        % endblock %

    </div>
% endblock %

模板\包含\form.html

% load form_tags widget_tweaks %

% if form.non_field_errors %
    <div class="alert alert-danger" role="alert">
        % for error in form.non_field_errors %
            <p% if forloop.last % class="mb-0"% endif %> error </p>
        % endfor %
    </div>
% endif %

% for field in form %
    <div class="form-group">

         field.label_tag 
        % render_field field class=field|input_class %

        % for error in field.errors %
            <div class="invalid-feedback">
                 error 
            </div>
        % endfor %

        % if field.help_text %
            <small class="form-text text-muted">
                 field.help_text|safe 
            </small>
        % endif %
        
    </div>
% endfor %

模板\password_reset.html

% extends 'base_accounts.html' %

% block title %Reset your password% endblock %

% block content %
    <div class="row justify-content-center">
        <div class="col-lg-4 col-md-6 col-sm-8">

            <div class="card">
                <div class="card-body">
                    <h3 class="card-title">Reset your password</h3>
                    <p>Enter your email address and we will send you a link to reset your password.</p>
                    <form method="post" novalidate>
                        % csrf_token %
                        % include 'includes/form.html' %
                        <button type="submit" class="btn btn-primary btn-block">Send password reset email</button>
                    </form>
                </div>
            </div>
            
        </div>
    </div>
% endblock %

模板\password_reset_complete.html

% extends 'base_accounts.html' %

% block title %Password changed!% endblock %

% block content %
    <div class="row justify-content-center">
        <div class="col-lg-6 col-md-8 col-sm-10">

            <div class="card">
                <div class="card-body">
                    <h3 class="card-title">Password changed!</h3>
                    <div class="alert alert-success" role="alert">
                        You have successfully changed your password! You may now proceed to log in.
                    </div>
                    <a href="% url 'login' %" class="btn btn-secondary btn-block">Return to log in</a>
                </div>
            </div>
            
        </div>
    </div>
% endblock %

模板\password_reset_confirm.html

% extends 'base_accounts.html' %

% block title %
    % if validlink %
        Change password for  form.user.username 
    % else %
        Reset your password
    % endif %
% endblock %

% block content %
    <div class="row justify-content-center">
        <div class="col-lg-6 col-md-8 col-sm-10">

            <div class="card">
                <div class="card-body">
                    % if validlink %
                        <h3 class="card-title">Change password for @ form.user.username </h3>
                        <form method="post" novalidate>
                            % csrf_token %
                            % include 'includes/form.html' %
                            <button type="submit" class="btn btn-success btn-block">Change password</button>
                        </form>
                    % else %
                        <h3 class="card-title">Reset your password</h3>
                        <div class="alert alert-danger" role="alert">
                            It looks like you clicked on an invalid password reset link. Please try again.
                        </div>
                        <a href="% url 'password_reset' %" class="btn btn-secondary btn-block">Request a new password reset link</a>
                    % endif %
                </div>
            </div>
            
        </div>
    </div>
% endblock %

模板\password_reset_done.html

% extends 'base_accounts.html' %

% block title %Reset your password% endblock %

% block content %
    <div class="row justify-content-center">
        <div class="col-lg-4 col-md-6 col-sm-8">
            
            <div class="card">
                <div class="card-body">
                    <h3 class="card-title">Reset your password</h3>
                    <p>Check your email for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder.</p>
                    <a href="% url 'login' %" class="btn btn-secondary btn-block">Return to log in</a>
                </div>
            </div>

        </div>
    </div>
% endblock %

模板\login.html

% extends 'base_accounts.html' %

% block title %Log in to Django Boards% endblock %

% block content %
    <div class="row justify-content-center">
        <div class="col-lg-4 col-md-6 col-sm-8">

            <div class="card">
                <div class="card-body">
                    <h3 class="card-title">Log in</h3>
                    <form method="post" novalidate>
                        % csrf_token %
                        % include 'includes/form.html' %
                        <button type="submit" class="btn btn-primary btn-block">Log in</button>
                    </form>
                </div>

                <div class="card-footer text-muted text-center">
                New to Django Boards? <a href="% url 'signup' %">Sign up</a>
                </div>
            </div>

            <div class="text-center py-2">
                <small>
                    <a href="#" class="text-muted">Forgot your password?</a>
                </small>
            </div>

        </div>
    </div>
% endblock %

【问题讨论】:

【参考方案1】:

我的项目\urls.py

更改此行url(r'^reset/(?P&lt;uidb64&gt;[0-9A-Za-z_\-]+)/(?P&lt;token&gt;[0-9A-Za-z]1,13-[0-9A-Za-z]1,20)/$'

url(r'^reset/&lt;uidb64&gt;/&lt;token&gt;/'

如果仍然卡住,请添加$url(r'^reset/&lt;uidb64&gt;/&lt;token&gt;/$'

【讨论】:

以上是关于Django 3.0.8 测试用例失败的主要内容,如果未能解决你的问题,请参考以下文章

Python接口测试实战4(下) - 框架完善:用例基类,用例标签,重新运行上次失败用例

TestNG中Appium测试用例的顺序执行导致测试用例失败

如果机器人框架中的第一个测试用例失败,如何跳过测试用例执行

Django 1.4 表单向导测试用例

H5用户登录测试用例

TestNG测试用例重跑详解及实践优化