How to Unit Test a Route in Flask - unit-testing

I've been experimenting with Flask, following a series by Corey Schafer on Youtube. I've tried to break off some of the concepts and build a very simplistic password application.
Thus far it works fine, but I'm wondering how I can build tests to insure that it's verifying password basic password validity. (THIS IS NOT TESTING PASSWORD MATCHING).
Obviously, in the application, it confirms if the password meets the minimum standards that being DataRequired() and Length(min=8)
But how I can confirm it's correct in a unittest module? I'm having trouble visualizing how to build that out...
You can find a repo here: https://github.com/branhoff/password-tester
The basic format of my code is primarily a Registration form and the actual flask route that renders the functionality.
password_tester.py
from flask import Flask, render_template, flash
from forms import RegistrationForm
app = Flask(__name__)
app.config['SECRET_KEY'] = '5791628bb0b13ce0c676dfde280ba245'
#app.route("/")
#app.route("/register", methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
flash(f'Sucess!', 'success')
return render_template('register.html', title='Register', form=form)
if __name__ == '__main__':
app.run(debug=True)
forms.py
from flask_wtf import FlaskForm
from wtforms import PasswordField, SubmitField
from wtforms.validators import DataRequired, Length
class RegistrationForm(FlaskForm):
password = PasswordField('Password', validators=[DataRequired(), Length(min=8)])
submit = SubmitField('Submit')
tests I've tried
I've tried the following code as a test. But the status code is always the same, no matter what I change the password to... so I'm not really understanding how to actually test the result.
def test_pass_correct(self):
tester = app.test_client(self)
response = tester.post('/register', data=dict(password=''))
self.assertEqual(response.status_code, 200)

It's not very elegant, but in my html template, I print out the error message if there is one. And I can test that the message is somewhere in the HTML statement.
So something like this seems to work:
# Ensure that the password-tester behaves correctly given correct credentials
def test_pass_correct(self):
tester = app.test_client(self)
response = tester.post('/register', data=dict(password='testtest'))
self.assertFalse(b'Field must be at least 8 characters long.' in response.data)
# Ensure that the password-tester behaves correctly given incorrect credentials
def test_pass_incorrect(self):
tester = app.test_client(self)
response = tester.post('/register', data=dict(password='test'))
self.assertTrue(b'Field must be at least 8 characters long.' in response.data)

Related

How to test Flask view context and templates using pytest?

I'm using pytest with Flask and want to test my views and templates but I'm unclear how best to do this.
I'm aware I can test the contents of the HTML output, e.g.:
def test_my_view(test_client):
# test_client is a fixture representing app.test_client()
response = test_client.get("/my-url")
assert b"<h1>My page title</h1>" in response.data
But there are things I'm not sure how best to do:
How do I test which template is being used by the view?
How do I test the context the view sends to the template? (e.g. check that login_form is an instance of LoginForm)
If I want to test that a more complex HTML tag is present, say a <form> tag with the correct action attribute, is the only way to check for the presence of the entire tag (e.g. <form method="get" class="form-lg" action="/other-url">) even if I'm not bothered about other attributes? How could I just check for the action, assuming other forms are on the page too?
I've realised that 1 and 2 can be solved by a solution like the one in this question, slightly altered for use with pytest.
Let's say we have this Flask view:
from flask import render_template
from app import app
#app.route("/my/view")
def my_view():
return render_template("my/template.html", greeting="Hello!")
We want to test that calling that URL uses the correct template, and that has the correct context data passed to it.
First, create a reusable fixture:
from flask import template_rendered
import pytest
#pytest.fixture
def captured_templates(app):
recorded = []
def record(sender, template, context, **extra):
recorded.append((template, context))
template_rendered.connect(record, app)
try:
yield recorded
finally:
template_rendered.disconnect(record, app)
I also have a test_client fixture for making requests in tests (something like the testapp fixture in Flask Cookiecutter or the test_client fixture in this tutorial).
Then write your test:
def test_my_view(test_client, captured_templates):
response = test_client.get("/my/view")
assert len(captured_templates) == 1
template, context = captured_templates[0]
assert template.name = "my/template.html"
assert "greeting" in context
assert context["greeting"] == "Hello!"
Note that you might have more than one element in captured_templates, depending on what your view does.

Only able to login ONCE with Selenium and Django Testing

I'm testing using Django's tests and Selenium so I can run tests with a live browser. But it won't let me log in more then once.
I am using Django's StaticLiveServerTestCase to access both static and media files live. I login by going to the page. I've tried putting the login code both in setUpClass to run once at the beginning and setUp to run each time. Both only let me log in once.
Here's the code:
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.contrib.auth.models import User
from selenium import webdriver
#override_settings(DEBUG=True)
class LoginTest(StaticLiveServerTestCase):
#classmethod
def setUpClass(cls):
super().setUpClass()
# create user
cls.user = User.objects.create_user( 'Testing User', 'somewhere#wherever.com', 'pwd')
# initalize webdriver
cls.driver = webdriver.Chrome() # doesn't work on FireFox either
cls.driver.set_window_size(1280,800)
cls.driver.implicitly_wait(5)
# login - send username and password to login page
cls.driver.get(cls.live_server_url+'/accounts/login/')
cls.driver.implicitly_wait(10)
username = cls.driver.find_element_by_name('login')
username.clear()
username.send_keys(cls.user.username)
password = cls.driver.find_element_by_name('password')
password.clear()
password.send_keys("pwd")
password.submit() # eubmits form
#classmethod
def tearDownClass(cls):
cls.driver.quit() # quit after tests have run
super().tearDownClass()
def test_login_one(self): # this test PASSES
self.driver.get(self.live_server_url) # go to home page
login_menu = self.driver.find_element_by_id('login_menu')
self.assertTrue(
# if logged in username is in text of #login_menu
self.user.username in login_menu.text
)
def test_login_two(self): # this test FAILS
self.driver.get(self.live_server_url) # go to home page
login_menu = self.driver.find_element_by_id('login_menu')
self.assertTrue(
# if logged in username is in text of #login_menu
self.user.username in login_menu.text
)
This code logs in once at the beginning. But I've also tried code that logs in each time a test is run (using setUp instead of 'setUpClass') and it still only lets me log in once.
Any idea what's going on?
Update:
I tried logging in a second time on test_log_in_two (the 2nd test) and I saw a "username and password not found" error in the chrome window.
What you are trying to achieve here is the capability of log in twice which is possible you just have to make a simple check inside your test method for presence of element after login happened, an if found you can simply logout and let the remaining code do it's work. Let me show you with a template what I am trying to say here :
#override_settings(DEBUG=True)
class LoginTest(StaticLiveServerTestCase):
#classmethod
def setUpClass(cls):
super().setUpClass()
logInFunction()
#classmethod
def tearDownClass(cls):
cls.driver.quit() # quit after tests have run
super().tearDownClass()
def test_login_one(self): # this test PASSES
if checkForAlreadyLoggedInElement() :
call logoutFunction()
logInFunction()
self.assertTrue(checkForAlreadyLoggedInElement())
def test_login_two(self):
if checkForAlreadyLoggedInElement() :
call logoutFunction()
logInFunction()
logoutFunction()
logInFunction()
self.assertTrue(checkForAlreadyLoggedInElement())
Hope the template clears the picture on how you should proceed. Let me know if you have any other doubts.

Django Testing - check messages for a view that redirects

I have been writing tests for one of my django applications and have been looking to get around this problem for quite some time now. I have a view that sends messages using django.contrib.messages for different cases. The view looks something like the following.
from django.contrib import messages
from django.shortcuts import redirect
import custom_messages
def some_view(request):
""" This is a sample view for testing purposes.
"""
some_condition = models.SomeModel.objects.get_or_none(
condition=some_condition)
if some_condition:
messages.success(request, custom_message.SUCCESS)
else:
messages.error(request, custom_message.ERROR)
redirect(some_other_view)
Now, while testing this view client.get's response does not contain the context dictionary that contains the messages as this view uses a redirect. For views that render templates we can get access to the messages list using messages = response.context.get('messages'). How can we get access messages for a view that redirects?
Use the follow=True option in the client.get() call, and the client will follow the redirect. You can then test that the message is in the context of the view you redirected to.
def test_some_view(self):
# use follow=True to follow redirect
response = self.client.get('/some-url/', follow=True)
# don't really need to check status code because assertRedirects will check it
self.assertEqual(response.status_code, 200)
self.assertRedirects(response, '/some-other-url/')
# get message from context and check that expected text is there
message = list(response.context.get('messages'))[0]
self.assertEqual(message.tags, "success")
self.assertTrue("success text" in message.message)
You can use get_messages() with response.wsgi_request like this (tested in Django 1.10):
from django.contrib.messages import get_messages
...
def test_view(self):
response = self.client.get('/some-url/') # you don't need follow=True
self.assertRedirects(response, '/some-other-url/')
# each element is an instance of django.contrib.messages.storage.base.Message
all_messages = [msg for msg in get_messages(response.wsgi_request)]
# here's how you test the first message
self.assertEqual(all_messages[0].tags, "success")
self.assertEqual(all_messages[0].message, "you have done well")
If your views are redirecting and you use follow=true in your request to the test client the above doesn't work. I ended up writing a helper function to get the first (and in my case, only) message sent with the response.
#classmethod
def getmessage(cls, response):
"""Helper method to return message from response """
for c in response.context:
message = [m for m in c.get('messages')][0]
if message:
return message
You include this within your test class and use it like this:
message = self.getmessage(response)
Where response is what you get back from a get or post to a Client.
This is a little fragile but hopefully it saves someone else some time.
I had the same problem when using a 3rd party app.
If you want to get the messages from a view that returns an HttpResponseRedict (from which you can't access the context) from within another view, you can use get_messages(request)
from django.contrib.messages import get_messages
storage = get_messages(request)
for message in storage:
do_something_with_the_message(message)
This clears the message storage though, so if you want to access the messages from a template later on, add:
storage.used = False
Alternative method mocking messages (doesn't need to follow redirect):
from mock import ANY, patch
from django.contrib import messages
#patch('myapp.views.messages.add_message')
def test_some_view(self, mock_add_message):
r = self.client.get('/some-url/')
mock_add_message.assert_called_once_with(ANY, messages.ERROR, 'Expected message.') # or assert_called_with, assert_has_calls...

How to Create Session Variables in Selenium/Django Unit Test?

I'm trying to write a functional test that uses Selenium to test a Django view. When the user comes to a page ("page2"), the view that renders that page expects to find a session variable "uid" (user ID). I've read a half dozen articles on how this is supposed to be done but none of them have worked for me. The code below shows how the Django documentation says it should be done but it doesn't work for me either. When I run the test, the view never completes executing and I get a "server error occurred" message. Could someone please tell me what I'm doing wrong? Thank you.
views.py:
from django.shortcuts import render_to_response
def page2(request):
uid = request.session['uid']
return render_to_response('session_tests/page2.html', {'uid': uid})
test.py:
from django.test import LiveServerTestCase
from selenium import webdriver
from django.test.client import Client
class SessionTest(LiveServerTestCase):
def setUp(self):
self.browser = webdriver.Firefox()
self.browser.implicitly_wait(3)
self.client = Client()
self.session = self.client.session
self.session['uid'] = 1
def tearDown(self):
self.browser.implicitly_wait(3)
self.browser.quit()
def test_session(self):
self.browser.get(self.live_server_url + '/session_tests/page2/')
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Page 2', body.text)
Here's how to solve this problem. James Aylett hinted at the solution when he mentioned the session ID above. jscn showed how to set up a session but he didn't mention the importance of the session key to a solution and he also didn't discuss how to link the session state to Selenium's browser object.
First, you have to understand that when you create a session key/value pair (e.g. 'uid'=1), Django's middleware will create a session key/data/expiration date record in your backend of choice (database, file, etc.). The response object will then send that session key in a cookie back to the client's browser. When the browser sends a subsequent request, it will send a cookie back that contains that key which is then used by the middleware to lookup the user's session items.
Thus, the solution required 1.) finding a way to obtain the session key that is generated when you create a session item and then; 2.) finding a way to pass that key back in a cookie via Selenium's Firefox webdriver browser object. Here's the code that does that:
selenium_test.py:
-----------------
from django.conf import settings
from django.test import LiveServerTestCase
from selenium import webdriver
from django.test.client import Client
import pdb
def create_session_store():
""" Creates a session storage object. """
from django.utils.importlib import import_module
engine = import_module(settings.SESSION_ENGINE)
# Implement a database session store object that will contain the session key.
store = engine.SessionStore()
store.save()
return store
class SeleniumTestCase(LiveServerTestCase):
def setUp(self):
self.browser = webdriver.Firefox()
self.browser.implicitly_wait(3)
self.client = Client()
def tearDown(self):
self.browser.implicitly_wait(3)
self.browser.quit()
def test_welcome_page(self):
#pdb.set_trace()
# Create a session storage object.
session_store = create_session_store()
# In pdb, you can do 'session_store.session_key' to view the session key just created.
# Create a session object from the session store object.
session_items = session_store
# Add a session key/value pair.
session_items['uid'] = 1
session_items.save()
# Go to the correct domain.
self.browser.get(self.live_server_url)
# Add the session key to the cookie that will be sent back to the server.
self.browser.add_cookie({'name': settings.SESSION_COOKIE_NAME, 'value': session_store.session_key})
# In pdb, do 'self.browser.get_cookies() to verify that it's there.'
# The client sends a request to the view that's expecting the session item.
self.browser.get(self.live_server_url + '/signup/')
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Welcome', body.text)
There are a couple of tickets in Django's bug tracker around this kind of problem, the main one seems to be: https://code.djangoproject.com/ticket/10899 which hasn't had any movement on it for a few months. Basically, you need to do some extra set up to get the session to work properly. Here's what worked for me (may not work as is with your particular set up, as I wasn't using Selenium):
def setUp(self):
from django.conf import settings
engine = import_module(settings.SESSION_ENGINE)
store = engine.SessionStore()
store.save()
self.client.cookies[settings.SESSION_COOKIE_NAME] = store.session_key
Now you should be able to access self.client.session and it should remember any changes you make to it.
Here is my solution for django==2.2.
from importlib import import_module
from django.conf import settings
from django.contrib.auth import get_user_model
# create the session database instance
engine = import_module(settings.SESSION_ENGINE)
session = engine.SessionStore()
# create the user and instantly login
User = get_user_model()
temp_user = User.objects.create(username='admin')
temp_user.set_password('password')
self.client.login(username='admin', password='password')
# get session object and insert data
session = self.client.session
session[key] = value
session.save()
# update selenium instance with sessionID
selenium.add_cookie({'name': 'sessionid', 'value': session._SessionBase__session_key,
'secure': False, 'path': '/'})

Django: test failing on a view with #login_required

I'm trying to build a test for a view that's decorated with
#login_required, since I failed to make it work, I did a simple test
and still can't make it pass.
Here is the code for the simple test and the view:
def test_login(self):
user = self._create_new_user()
self.assertTrue(user.is_active)
login = self.client.login(username=user.username,
password=self.data['password1'])
self.failUnless(login, 'Could not log in')
response = self.client.get('/accounts/testlogin/')
self.assertEqual(response.status_code, 200)
#login_required
def testlogin(request):
print 'testlogin !! '
return HttpResponse('OK')
_create_new_user() is saving the user and there is a test inside that
method to see that is working.
The test fails in the response.status_code, returning 302 and the
response instance is of a HttpResponseRedirect, is redirecting it as
if not logged in.
Any clue? I'm missing something?
Regards
Esteban
This testcase works for me:
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.test.client import Client
import unittest
class LoginTestCase(unittest.TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('john', 'lennon#thebeatles.com', 'johnpassword')
def testLogin(self):
self.client.login(username='john', password='johnpassword')
response = self.client.get(reverse('testlogin-view'))
self.assertEqual(response.status_code, 200)
I suggest you (if you don't use them already) to use the reverse() function and name your URLs. This way you are sure that you get always the right URL.
Here is the answer:
Python 2.6.5 made a change to the way
cookies are stored which is subtly
incompatible with the test client.
This problem has been fixed in the
1.1.X and trunk branches, but the fix hasn't yet made it into a formal
release.
If you are using 1.1.X and Python
2.6.5, you're going to have problems with any test activity involving
cookies. You either need to downgrade
Python, or use the 1.1.X branch rather
than the 1.1.1 release.
A 1.1.2 release (that will include the
fix for the problem you describe) will
be made at the same time that we
release 1.2 - hopefully, very very
soon.
Yours, Russ Magee %-)
http://groups.google.com/group/django-users/browse_frm/thread/617457f5d62366ae/05f0c01fff0b9e6d?hl=en&lnk=gst&q=2.6.5#05f0c01fff0b9e6d
OK I was to facing same problem #resto solved my problem.
creating user this way below, lets the test client get the user logged in and get the response other than redirect (302)
self.user = User.objects.create_user('john', 'lennon#thebeatles.com', 'johnpassword')