According to the documentation, --reuse-db should be used or addopts = --reuse-db in pytest.ini. I tried both and they don't work. The current tests have to signup and authenticate a new user at the start of each test to be able to access features requiring login. This makes tests run slowly and with the number of tests growing, this is becoming less convenient. Here's an example to demonstrate:
#pytest.mark.django_db
class TestSignIn:
def test_valid_user(self, client, valid_user_data, django_user_model, django_db_keepdb):
client_signup(client, valid_user_data, django_db_keepdb)
response = activate_user(client)
assert 'sign out' in get_text(response)
def test_sign_in(self, client, valid_user_data, django_user_model, django_db_keepdb):
client_signup(client, valid_user_data)
activate_user(client)
response = client.post(reverse('signout'), follow=True)
assert 'sign in' in get_text(response)
response = client_sign_in(client, valid_user_data)
assert 'sign out' in get_text(response)
In test_valid_user, a new user is created and after user activation, a user shows up:
>>> django_user_model.objects.count()
1
and I verify using:
>>> django_db_keepdb
True
I want to reuse the user created in the previous test instead of creating a new one. With --reuse-db specified, I'm expecting the second test test_sign_in to detect the very same user. Instead, I get:
>>> django_user_model.objects.count()
0
#pytest.mark.django_db marker runs the function-scoped _django_db_helper fixture.
Implement #pytest.mark.django_db_class_scope marker that runs a class-scoped fixture:
import pytest
from pytest_django.fixtures import _django_db_helper
_django_db_function_scope_helper = pytest.fixture(_django_db_helper.__wrapped__, scope='function')
_django_db_class_scope_helper = pytest.fixture(_django_db_helper.__wrapped__, scope='class')
#pytest.fixture()
def _django_db_helper(request) -> None:
marker = request.node.get_closest_marker('django_db_class_scope')
if not marker:
request.getfixturevalue('_django_db_function_scope_helper')
#pytest.fixture(autouse=True)
def django_db_class_scope_marker(request) -> None:
marker = request.node.get_closest_marker('django_db_class_scope')
if marker:
request.getfixturevalue('_django_db_class_scope_helper')
Usage:
# #pytest.mark.django_db
#pytest.mark.django_db_class_scope
class TestSignIn:
def test_valid_user(self, django_user_model):
django_user_model.objects.create()
assert django_user_model.objects.count() == 1
def test_sign_in(self, django_user_model):
assert django_user_model.objects.count() == 1
The problem is that the django_db_keepdb fixture is used to indicate that the database should not be destroyed after the test has completed. It does not prevent the database from being reset between tests. In order to reuse the same user between tests, you will need to use the django_db_reset_sequences fixture. This fixture will reset the database and keep the data, allowing you to reuse the same data in subsequent tests.
django_db_reset_sequences fixture in action:
#pytest.mark.django_db
class TestSignIn:
def test_valid_user(self, client, valid_user_data, django_user_model, django_db_reset_sequences):
client_signup(client, valid_user_data)
response = activate_user(client)
assert 'sign out' in get_text(response)
def test_sign_in(self, client, valid_user_data, django_user_model, django_db_reset_sequences):
client_signup(client, valid_user_data)
activate_user(client)
response = client.post(reverse('signout'), follow=True)
assert 'sign in' in get_text(response)
response = client_sign_in(client, valid_user_data)
assert 'sign out' in get_text(response)
You will also need to include the django_db_reset_sequences fixture in the list of arguments for each test function. This will reset the database and keep the data between tests, allowing you to reuse the same user in both tests.
Related
I want to know how am I supposed to test my code and see whether it works properly. I want to make sure that it stores the received data to the database. Can you please tell me how am I supposed to do that? While I was searching the forum I found this post but I did not really understand what is going on. here is the code I want to test.
client = MongoClient(os.environ.get("MONGODB_URI"))
app.db = client.securify
app.secret_key = str(os.environ.get("APP_SECRET"))
#app.route("/", methods=["GET", "POST"])
def home():
if request.method == "POST":
ip_address = request.remote_addr
entry_content = request.form.get("content")
formatted_date = datetime.datetime.today().strftime("%Y-%m-%d/%H:%M")
app.db.entries.insert({"content": entry_content, "date": formatted_date, "IP": ip_address})
return render_template("home.html")
and here is the mock test I wrote:
import os
from unittest import TestCase
from app import app
class AppTest(TestCase):
# executed prior to each test
def setUp(self):
# you can change your application configuration
app.config['TESTING'] = True
# you can recover a "test cient" of your defined application
self.app = app.test_client()
# then in your test method you can use self.app.[get, post, etc.] to make the request
def test_home(self):
url_path = '/'
response = self.app.get(url_path)
self.assertEqual(response.status_code, 200)
def test_post(self):
url_path = '/'
response = self.app.post(url_path,data={"content": "this is a test"})
self.assertEqual(response.status_code, 200)
The test_post gets stuck and after some seconds gives an error when reaches app.db.entries.insert({"content": entry_content, "date": formatted_date, "IP": ip_address}) part. Please tell me also how can I retrieve the saved data in order to make sure it is saved in the expected way
This is what I do using NodeJS, not tested at all in python but the idea is the same.
First of all, find a in-memory DB, there are options like pymongo-inmemory or mongomock
Then in your code you have to do the connection according to you environment (production/development/whatever)
Something like this:
env = os.environ.get("ENV")
if env == "TESTING":
# connect to mock db
elif env == "DEVELOMPENT":
# for example if you want to test against a real DB but not the production one
# then do the connection here
else:
# connect to production DB
I don't know if it is the proper way to do it but I found a solution. After creating a test client self.app = app.test_client() the db gets set to localhost:27017 so I changed it manually as follows and it worked:
self.app = app.test_client()
client = MongoClient(os.environ.get("MONGODB_URI"))
I am new to django unittest and pytest. However, I started to feel that pytest test case is more compact and clearer.
Here is my test cases:
class OrderEndpointTest(TestCase):
def setUp(self):
user = User.objects.create_superuser(username='admin', password='password', email='pencil#gmail.com')
mommy.make(CarData, _quantity=1)
mommy.make(UserProfile, _quantity=1, user=user)
def test_get_order(self):
mommy.make(Shop, _quantity=1)
mommy.make(Staff, _quantity=1, shop=Shop.objects.first())
mommy.make(Order, _quantity=1, car_info={"color": "Black"}, customer={"name": "Lord Elcolie"},
staff=Staff.objects.first(), shop=Shop.objects.first())
factory = APIRequestFactory()
user = User.objects.get(username='admin')
view = OrderViewSet.as_view({'get': 'list'})
request = factory.get('/api/orders/')
force_authenticate(request, user=user)
response = view(request)
assert 200 == response.status_code
assert 1 == len(response.data.get('results'))
And here is the pytest version
def test_get_order(car_data, admin_user, orders):
factory = APIRequestFactory()
user = User.objects.get(username='admin')
view = OrderViewSet.as_view({'get': 'list'})
request = factory.get('/api/orders/')
force_authenticate(request, user=user)
response = view(request)
assert 200 == response.status_code
assert 1 == len(response.data.get('results'))
The benefit from pytest is fixture in another file. It makes my test case compact by let them be my input parameters.
Are they any benefit of using Django unittest than pytest?
Update: 1July2017
Update: 5July2017
Update: 1Sep2017
Update: 29Sep2017
Update: 26Dec2017
Pytest reduces your problem when fixtures got mutated over the test.
I got testcases that run individually passed, but fail when run
thoroughly.
Pytest will show you the assertion output if the error occur. Django
unittest does not. I have to put the breakpoint on my own and
investigate the error.
Pytest allow you to use real database with simple decorator. Django
test does not. You have to create your own customized command for
your job
Pytest is generic. Being an generic it means you feel comfortable to
work with project outside the Django. For example when you have to
build micro-service such as Flask + 3rd parties like APScheduler,
PyRad, ... etc. I mention this because my backend life uses Django 50%
The rest of the is Python and infra
Pytest is not using multiple inheritance to create my fixtures
Unittest takes advantage on gitlab-ci over Pytest when used with Docker as a runner by smoothly execute without any extra configurations. problem
i've used Django test for my entire life and now i am using Py.test. I agree that pytest is much cleaner than django itself.
The benefit from pytest is fixture in another file. It makes my test case compact by let them be my input parameters.
In Django unittest you can still use fixtures in other file by using the attribute fixtures = ['appname/fixtures/my_fixture.json']
Pytest will show you the assertion output if the error occur. Django unittest does not. I have to put the breakpoint on my own and investigate the error.
Did you tried to change the --verbose param on python manage.py tests?
A few tips:
There is a package called pytest-django that will help you integrate and using django with pytest.
I think that if you use classes will you not need to use the factory = APIRequestFactory(), the test methods itself they have a parameter called client that is a interface to the python requests module to access your views.
import pytest
from model_mommy import mommy
#pytest.fixture()
def user(db):
return mommy.make(User)
class SiteAPIViewTestSuite:
def test_create_view(self, client, user):
assert Site.objects.count() == 0
post_data = {
'name': 'Stackoverflow'
'url': 'http://stackoverflow.com',
'user_id': user.id,
}
response = client.post(
reverse('sites:create'),
json.dumps(post_data),
content_type='application/json',
)
data = response.json()
assert response.status_code == 201
assert Site.objects.count() == 1
assert data == {
'count': 1,
'next': None,
'previous': None
'results': [{
'pk': 1,
'name': 'Stackoverflow',
'url': 'http://stackoverflow.com',
'user_id': user.id
}]
}
I'm trying out Channels in Django 1.10 and set up a few consumers.
I tried creating a login_required decorator for it that closes the connection before executing it to prevent guests from entering this private socket. Also integrated unit tests afterwards to test it and they keep failing because it keeps letting guests in (AnonymousUser errors everywhere).
Also, sometimes when logging in and logging out the session doesn't clear and it lets the old user in.
The decorator:
def login_required_websocket(func):
"""
If user is not logged in, close connection immediately.
"""
#functools.wraps(func)
def inner(message, *args, **kwargs):
if not message.user.is_authenticated():
message.reply_channel.send({'close': True})
return func(message, *args, **kwargs)
return inner
Here's the consumer code:
def ws_connect(message, slug):
message.reply_channel.send({ 'accept': True })
client = message.reply_channel
client.send(signal.message("Welcome"))
try:
# import pdb; pdb.set_trace()
Room.objects.get(name=slug)
except Room.DoesNotExist:
room = Room.objects.create(name=slug)
room.users.add(message.user)
room.turn = message.user.id
room.save()
story = Story(room=room)
story.save()
# We made sure it exists.
room = Room.objects.get(name=slug)
message.channel_session['room'] = room.name
# Check if user is allowed here.
if not room.user_allowed(message.user):
# Close the connection. User is not allowed.
client.send(Signal.error("User isn't allowed in this room."))
client.send({'close': True})
The strange thing is, when commenting out all the logic between client.send(signal.message)) forwards, it works just fine and unit tests pass (meaning guests are blocked and auth code does not run [hence AnonymousUser errors]). Any ideas?
Here's the tests too:
class RoomsTests(ChannelTestCase):
def test_reject_guest(self):
"""
This tests whether the login_required_websocket decorator is rejecting guests.
"""
client = HttpClient()
user = User.objects.create_user(
username='test', password='password')
client.send_and_consume('websocket.connect',
path='/rooms/test_room', check_accept=False)
self.assertEqual(client.receive(), {'close': True})
def test_accept_logged_in(self):
"""
This tests whether the connection is accepted when a user is logged in.
"""
client = HttpClient()
user = User.objects.create_user(
username='test', password='password')
client.login(username='test', password='password')
client.send_and_consume('websocket.connect', path='/rooms/test_room')
Am I approaching this wrong, and if I am, how do I do this (require auth) properly?
EDIT: Integrated an actions system to try something out, looks like Django channels is simply not picking up any sessions from HTTP at all.
#enforce_ordering
#channel_session_user_from_http
def ws_connect(message, slug):
message.reply_channel.send({'accept': True})
message.reply_channel.send(Action.info(message.user.is_authenticated()).to_send())
Just returns false.
EDIT2: I see it works now, I tried changing localhost to 127.0.0.1 and turns out it works now. Is there a way to make it detect localhost as a valid domain so it ports over the sessions?
EDIT3: Turns out I found the localhost vs 127.0.0.1 cookie issue haha. To not waste the bounty, how would you personally implement auth login_required in messages/channels?
edit4: While I still don't know why the thing didn't work, here's how I eventually changed my app around the issue:
I created an actions system. When entering in, the socket does nothing until you send it an AUTHENTICATE action through JSON. I separated logged in actions in guest_actions and user_actions. Once authenticated, it sets the session and you are able to use user_actions.
Django Channels already supports session authentication:
# In consumers.py
from channels import Channel, Group
from channels.sessions import channel_session
from channels.auth import channel_session_user, channel_session_user_from_http
# Connected to websocket.connect
#channel_session_user_from_http
def ws_add(message):
# Accept connection
message.reply_channel.send({"accept": True})
# Add them to the right group
Group("chat-%s" % message.user.username[0]).add(message.reply_channel)
# Connected to websocket.receive
#channel_session_user
def ws_message(message):
Group("chat-%s" % message.user.username[0]).send({
"text": message['text'],
})
# Connected to websocket.disconnect
#channel_session_user
def ws_disconnect(message):
Group("chat-%s" % message.user.username[0]).discard(message.reply_channel)
http://channels.readthedocs.io/en/stable/getting-started.html#authentication
Your function worked "as-is" for me. Before I walk through the details, there was a bug (now resolved) that was preventing sessions from being closed which may explain your other issue.
I use scarce quotes around "as-is" because I was using a class-based consumer so I had to add self to the whole stack of decorators to test it explicitly:
class MyRouter(WebsocketDemultiplexer):
# WebsocketDemultiplexer calls raw_connect for websocket.connect
#channel_session_user_from_http
#login_required_websocket
def raw_connect(self, message, **kwargs):
...
After adding some debug messages to verify the sequence of execution:
>>> ws = create_connection("ws://localhost:8085")
# server logging
channel_session_user_from_http.run
login_required_websocket.run
user: AnonymousUser
# client logging
websocket._exceptions.WebSocketBadStatusException: Handshake status 403
>>> ws = create_connection("ws://localhost:8085", cookie='sessionid=43jxki76cdjl97b8krco0ze2lsqp6pcg')
# server logging
channel_session_user_from_http.run
login_required_websocket.run
user: admin
As you can see from my snippet, you need to call #channel_session_user_from_http first. For function-based consumers, you can simplify this by including it in your decorator:
def login_required_websocket(func):
#channel_session_user_from_http
#functools.wraps(func)
def inner(message, *args, **kwargs):
...
On class-based consumers, this is handled internally (and in the right order) by setting http_user_and_session:
class MyRouter(WebsocketDemultiplexer):
http_user_and_session = True
Here's the full code for a self-respecting decorator that would be used with it:
def login_required_websocket(func):
"""
If user is not logged in, close connection immediately.
"""
#functools.wraps(func)
def inner(self, message, *args, **kwargs):
if not message.user.is_authenticated():
message.reply_channel.send({'close': True})
return func(self, message, *args, **kwargs)
return inner
My suggestion is that you can require a session key or even better take the username/password input within your consumer method. Then call the authenticate method to check if the user exists. On valid user object return, you can broadcast the message or return and invalid login details message.
from django.contrib.auth import authenticate
#channel_session_user
def ws_message(message):
user = authenticate(username=message.username, password=message.password')
if user is not None:
Group("chat-%s" % message.user.username[0]).send({
"text": message['text'],
})
else:
# User is not authenticated so return an error message.
I am getting an error when I try to set the attribute for authenticated_userid as a request parameter. Its actually a nosetest I am using to mock up the request and see the response.
Traceback (most recent call last):
File "/web/core/pulse/wapi/tests/testWapiUtilities_integration.py", line 652, in setUp
setattr(self.request, 'authenticated_userid', self.data['user'].id)
AttributeError: can't set attribute
Code is as below
#attr(can_split=False)
class logSuspiciousRequestAndRaiseHTTPError(IntegrationTestCase):
def setUp(self):
super(logSuspiciousRequestAndRaiseHTTPError, self).setUp()
from pyramid.request import Request
from pyramid.threadlocal import get_current_registry
request = Request({
'SERVER_PROTOCOL': 'testprotocol',
'SERVER_NAME': 'test server name',
'SERVER_PORT': '80',
})
request.context = TestContext()
request.root = request.context
request.subpath = ['path']
request.traversed = ['traversed']
request.view_name = 'test view name'
request.path_info = 'test info'
request.scheme = 'https'
request.host = 'test.com'
request.registry = get_current_registry()
self.request = request
self.data = {}
self.createDefaultData()
self.request.userAccount = self.data['user'].userAccount
# #unittest.skip('Pre-Demo skip. Need to mock userAccountModel')
#mock.patch('pulse.wapi.wapiUtilities.pyramid.threadlocal.get_current_request')
#mock.patch('pulse.wapi.wapiUtilities.securityLog')
def testHasRequest_raises400AndLogsError(
self, securityLog, get_current_request):
# Arrange
get_current_request.return_value = self.request
with self.assertRaises(exception.HTTPBadRequest):
from pulse.wapi.wapiUtilities import logSuspiciousRequestAndRaiseHTTPError
logSuspiciousRequestAndRaiseHTTPError()
self.assertTrue(securityLog.called)
self.assertTrue(securityLog.return_value.info.called)
I am creating a dummy request and I am adding attributes to request.
When this method logSuspiciousRequestAndRaiseHTTPError() is called the request is parsed by the method to get user account.
userAccountID=authenticated_userid(self.request)
This returns None since the request doesn't have an attribute self.request.authenticated_userid
Please let me know if you need any additional information.
Finally I got the solution.
I added self.config = testing.setUp()
self.config.testing_securitypolicy(
userid=self.data['user'].userAccount.id, permissive=True
)
Added the userAccountId as mock up value for testing security policy.
#attr(can_split=False)
class logSuspiciousRequestAndRaiseHTTPError(IntegrationTestCase):
def setUp(self):
super(logSuspiciousRequestAndRaiseHTTPError, self).setUp()
from pyramid.request import Request
from pyramid.threadlocal import get_current_registry
self.config = testing.setUp()
request = Request({
'SERVER_PROTOCOL': 'testprotocol',
'SERVER_NAME': 'test server name',
'SERVER_PORT': '80',
})
request.context = TestContext()
request.root = request.context
request.subpath = ['path']
request.traversed = ['traversed']
request.view_name = 'test view name'
request.path_info = 'test info'
request.scheme = 'https'
request.host = 'test.com'
request.registry = get_current_registry()
self.request = request
self.data = {}
self.createDefaultData()
self.request.userAccount = self.data['user'].userAccount
#mock.patch('pulse.wapi.wapiUtilities.pyramid.threadlocal.get_current_request')
#mock.patch('pulse.wapi.wapiUtilities.securityLog')
def testHasRequest_raises400AndLogsError(
self, securityLog, get_current_request):
# Arrange
get_current_request.return_value = self.request
self.loggedInUser = self.data['user']
self.config.testing_securitypolicy(
userid=self.data['user'].userAccount.id, permissive=True
)
with self.assertRaises(exception.HTTPBadRequest):
from pulse.wapi.wapiUtilities import logSuspiciousRequestAndRaiseHTTPError
logSuspiciousRequestAndRaiseHTTPError()
self.assertTrue(securityLog.called)
self.assertTrue(securityLog.return_value.info.called)
authenticated_userid is reified attribute set by authentication framework.
See Logins with authentication for basic information.
Please include more code how you set up your request, as in its current form the question does not have details to give accurate answer.
Because authenticated_userid is a reified attribute coming from the underlying authentication policy, it can not be directly set in the DummyRequest when doing a test. This means both of the below will not work:
# Will NOT work
dummy_request = DummyRequest(authenticated_userid='mock_user')
# Also will NOT work
dummy_request = DummyRequest()
dummy_request.authenticated_userid = 'mock_user'
Instead, if we want to be able to control the authenticated_userid for a test (or other aspects of the auth policies as well), we need to change the underlying Pyramid config for the test we're running. For this, you'll want to take a look at pyramid.testing.setUp (docs here). This returns a config object that can do a whole bunch of things, but the important one for our interests is the testing_securitypolicy method (docs here).
testing_securitypolicy allows us to have pretty granular control over how requests will be seen from an auth point of view. Look at its docs for specifics, but with it we can set what the authenticated_userid will be for a request, make it so that permission requirements are ignored, and more.
Here's an example of usage in a test:
from pyramid.testing import (setUp, tearDown, DummyRequest)
def test_some_view():
config = setUp()
config.testing_securitypolicy(userid='mock_user') # Sets authenticated_userid
dummy_request = DummyRequest()
print(dummy_request.authenticated_userid) # Prints 'mock_user'
# Now ready to test something that uses request.authenticated_userid
from mypyramidapp.views.secure import some_auth_view
result = some_auth_view(dummy_request)
expected = 'Hello mock_user!'
assert result == expected
# Finally, to avoid security changes leaking to other tests, use tearDown
tearDown() # Undo the effects of pyramid.testing.setUp()
I am writing test cases for my CRUD flask app https://github.com/Leo-g/Flask-Skeleton/blob/master/tests.py
I want to ensure that the update and delete tests run only if the add tests succeeds.
def test_add(self):
rv = self.app.post('/users/add', data=dict(name = 'test name', email = 'test#email.com'), follow_redirects=True)
assert 'Add was successful' in rv.data.decode('utf-8')
def test_Update(self):
with app.app_context():
id = Users.query.first().id
rv = self.app.post('/users/update/{}'.format(id), data=dict(name = 'test name update', email = 'test#email.update'), follow_redirects=True)
assert 'Update was successful' in rv.data.decode('utf-8')
While reading the docs I see that this can be done via the unittest.skip decorator but I am not sure How I can implement it.
You can use the --failfast command-line option when running the tests to make the test suite stop after a single test fails. For example, if you normally run the tests with the command python -m unittest you can change it to python -m unittest --failfast.