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.
Related
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.
Hi I am new to unit testing and I am digging into mocks and pytest.I am trying to unit test two rest api requests where the GET request checks if an item doesn't exist in the API and if it does not create it with POST request and to create a folder, otherwise if it exists just to create a folder.
I tried to use Mocker() and I am stuck on AttributeError: Mocker when I try to mock the GET request.
This is the code I am trying to test:
client = requests.session()
# get item by name
response = client.get(
f"https://url.com/rest/item/info?name=test_item",
auth=username, password),
)
if (
response.status_code == 200
and response.json()["status"]
== "Item doesn't exist!"
):
logging.info(f"Creating item")
client.post(
"https://url.com/rest/item/create?name=test_item",
auth=username, password),
)
# directory creation
dir_make = "mkdir -p test_item/temperature"
exec_cmd(dir_make)
elif response.status_code == 200 and response.json()["status"]=="OK":
# directory creation
dir_make = "mkdir -p test_item/temperature"
exec_cmd(dir_make)
And this is the unit test that fails with AttributeError:
def test_existing_item(requests_mock, monkeypatch):
with requests_mock.Mocker() as mock:
mock.get("https://url.com/rest/item/info?name=test_item", text="OK")
resp = requests.get("https://url.com/rest/item/info?name=test_item")
assert resp.text == "OK"
EDIT: Test for item not found and POST mock. It seems like it doesn't add coverage to the else statement. How can be tested if the item exists and only the folder needs to be added in that case?
EDIT 2: Added elif statement instead of else and 2 separate tests, still the one test_existing_items() doesn't cover the elif statement...What am I doing wrong in that case?
def test_existing_item(monkeypatch):
with requests_mock.Mocker() as mock_request:
mock_request.get(requests_mock.ANY, text="success!")
resp = requests.get(
"https://url.com/rest/item/info?name=test_item",
auth=("mock_username", "mock_password"),
)
if resp.status_code == 200 and resp.json()["status"] == "OK":
dir_make = "mkdir -p test_item/temperature"
exec_cmd(dir_make)
encoded_auth = b64encode(b"mock_username:mock_password").decode("ascii")
assert mock_request.last_request.headers["Authorization"] == f"Basic {encoded_auth}"
def test_post_item(monkeypatch):
with requests_mock.Mocker() as mock_request:
mock_request.get(requests_mock.ANY, text="success!")
resp = requests.get(
"https://url.com/rest/item/info?name=test_item",
auth=("mock_username", "mock_password"),
)
if resp.status_code == 200 and resp.json()["status"] == "ERROR":
mock_request.get(requests_mock.ANY, text="success!")
requests.post(
"https://url.com/rest/item/create?name=test_item",
auth=("mock_username", "mock_password"),
)
dir_make = "mkdir -p test_item/temperature"
exec_cmd(dir_make)
encoded_auth = b64encode(b"mock_username:mock_password").decode("ascii")
assert mock_request.last_request.headers["Authorization"] == f"Basic {encoded_auth}"
I am not familiar with unit testing so any help would be appreciated to unit test this code.
import requests
import requests_mock
def test_existing_item(monkeypatch):
with requests_mock.Mocker() as mock:
mock.get("https://url.com/rest/item/info?name=test_item", text="OK")
resp = requests.get("https://url.com/rest/item/info?name=test_item")
assert resp.text == "OK"
Don't pass requests_mock as parameter and pytest should work fine.
EDIT:
As for your edit:
It seems like it doesn't add coverage to the else statement. How can be tested if the item exists and only the folder needs to be added in that case?
That would be because your if condition is always true, so it never accesses the code below the else statement. Your second question is rather unclear to me, but I believe you want to write several tests, one for your if statement and one for your else statement. As a rule of thumb, if you need conditional logic in your test, you have a problem: either everything should go as you want it to every time you run your tests, either you should abort and have the test fail, as you want your code to have the exact same behavior every time you run it - and your tests as well, by extension.
I'm running tests in a Django project called lucy-web. If I run a simple python manage.py test, all tests pass:
(venv) Kurts-MacBook-Pro:lucy-web kurtpeek$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
........................
----------------------------------------------------------------------
Ran 24 tests in 22.097s
OK
Destroying test database for alias 'default'...
However, if I try to run a particular test defined in lucy_web/tests/test_schedule_request.py, it fails:
(venv) Kurts-MacBook-Pro:lucy-web kurtpeek$ python manage.py test lucy_web.tests.test_schedule_request
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.F
======================================================================
FAIL: test_send_schedule_request (lucy_web.tests.test_schedule_request.ScheduleRequestTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/kurtpeek/Documents/Dev/lucy/lucy-web/venv/lib/python3.6/site-packages/freezegun/api.py", line 495, in wrapper
result = func(*args, **kwargs)
File "/Users/kurtpeek/Documents/Dev/lucy/lucy-web/lucy_web/tests/test_schedule_request.py", line 44, in test_send_schedule_request
self.assertEqual(mail.outbox[0].body, expected_body)
AssertionError: 'Hi!\[17 chars]ily 1) wants to schedule a Birth Preparation a[102 chars] AM.' != 'Hi!\[17 chars]ily 12) wants to schedule a Birth Preparation [103 chars] AM.'
Hi!
- Test Test (family 1) wants to schedule a Birth Preparation and Preferences session within the next day or two.
+ Test Test (family 12) wants to schedule a Birth Preparation and Preferences session within the next day or two.
? +
Test requested this session on 01/01/12 at 09:00 AM.
----------------------------------------------------------------------
Ran 2 tests in 2.365s
FAILED (failures=1)
Destroying test database for alias 'default'...
Here is the offending test as defined in test_schedule_request.py:
import json
from django.conf import settings
from django.contrib.auth.models import User
from django.core import mail
from django.test import Client, TestCase
from freezegun import freeze_time
from ..models import Company, Family, Package, SessionType, Session, SessionCategory
class ScheduleRequestTest(TestCase):
def setUp(self):
self.client = Client()
self.url = '/api/v1.0/sessions/send_schedule_request/'
#freeze_time("2012-01-01 17:00:00")
def test_send_schedule_request(self):
user = User.objects.create_user('john', 'lennon#thebeatles.com', 'johnpassword')
user.first_name = "Test"
user.last_name = "Test"
user.save()
company = Company.objects.create(name='test company')
package = Package.objects.create(name='test package', company=company)
family = Family.objects.create(employee_user=user, employee_first_name='Test', employee_last_name='test', employee_email='test#test.net', package=package, point_of_contact='Employee')
category = SessionCategory.objects.create(name="Birth Prep")
session_type = SessionType.objects.create(category=category, title='Birth Preparation and Preferences', recommended_timing='32-36 weeks', min_duration=60, max_duration=90, allow_virtual=True, allow_in_person=True, description='some stuff', what_to_expect='other stuff')
session = Session.objects.create(session_number=1, family=family, session_type=session_type, location=Session.OTHER, other_location='spa', feedback_expert='YES', feedback_session='YES')
self.client.login(username='john', password='johnpassword')
response = self.client.post(self.url, json.dumps({'session_id': session.id, 'timeframe': 'within the next day or two'}), content_type="application/json")
self.assertEqual(response.status_code, 200)
# Check that session status has been updated
self.assertEqual(Session.objects.filter(id=session.id)[0].status, 'Times Requested')
# Check that two emails have been sent.
self.assertEqual(len(mail.outbox), 2)
# Verify that the contents of the emails are correct
self.assertEqual(mail.outbox[0].subject, 'LUCY: Request to Schedule Session')
self.assertEqual(mail.outbox[0].from_email, settings.DEFAULT_FROM_EMAIL)
self.assertEqual(mail.outbox[0].to, [settings.CUSTOMER_SERVICE_EMAIL])
self.assertEqual(mail.outbox[0].reply_to, [user.email])
expected_body = "Hi!\n\nTest Test (family 12) wants to schedule a Birth Preparation and Preferences session within the next day or two.\n\nTest requested this session on 01/01/12 at 09:00 AM."
self.assertEqual(mail.outbox[0].body, expected_body)
self.assertEqual(mail.outbox[1].subject, 'LUCY: Request to Schedule Session')
self.assertEqual(mail.outbox[1].from_email, settings.DEFAULT_FROM_EMAIL)
self.assertEqual(mail.outbox[1].to, [user.email])
self.assertEqual(mail.outbox[1].reply_to, [settings.CUSTOMER_SERVICE_EMAIL])
expected_body = "Hi Test,\n\nWe received your request for Birth Preparation and Preferences and our team is currently working on scheduling it for you. We will confirm the expert, date & time within 48 hours (or sooner if your request was urgent) via this email address.\n\nIf you have any questions, simply reply to this email and someone from our team will get back to you ASAP!\n\nBest,\nLUCY team"
self.assertEqual(mail.outbox[1].body, expected_body)
I've checked using the --verbosity option that this test was run successfully when I was running all tests. The only explanation I can think of is that the tests are somehow not isolated from each other, and the success of one depends on the setUp of the other.
Could this be the case? Do I perhaps have to write some tearDown methods in the previous test cases? Or does Django run each test on a new database, so that this should not be necessary?
It appears that the code that created the expected_body refers to the an id or other similar field. If the full test suite is run it will keep using the next unique number when it creates the test (in the full suite it will be the 12th creation).
When running the test by itself it is therefore the 1st creation.
You should refer to the id when creating the string e.g.:
expected_body = "Hi!\n\nTest Test (family %s) wants to schedule a Birth Preparation and Preferences session within the next day or two.\n\nTest requested this session on 01/01/12 at 09:00 AM." % family.id
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 am writing an api in Flask. I have couple of views that return json responses and I have written some unit tests that will check if those views are working properly and are returning correct data. Then I turned on coverage plugin for nosetests (in my case nose-cov).
And here's where my problem starts, coverage is not seeing my views as being executed by tests.
First some base code to give you full picture:
My view:
def get_user(uid):
"""Retrieve user.
Args:
uid (url): valid uid value
Usage: ::
GET user/<uid>/
Returns:
obj:
::
{
'data': {
`response.User`,
},
'success': True,
'status': 'get'
}
"""
if not uid:
raise exception.ValueError("Uid is empty")
obj = db_layer.user.get_user(uid=value)
return {
'data': {
obj.to_dict(), # to_dict is helper method that converts part of orm into dict
},
'success': True,
'status': 'get'
}
My test:
class TestUserViews(base.TestViewsBase):
def test_get_user(self):
uid = 'some_uid_from_fixtures'
name = 'some_name_from_fixtures'
response = self.get(u'user/uid/{}/'.format(uid))
self.assertEqual(response.status_code, 200)
user_data = json.loads(response.text)['data']
self.assertEqual(name, user_data['username'])
self.assertEqual(uid, user_data['uid'])
def get(self, method, headers=None):
"""
Wrapper around requests.get, reassures that authentication is
sorted for us.
"""
kwargs = {
'headers': self._build_headers(headers),
}
return requests.get(self.get_url(method), **kwargs)
def get_url(self, method):
return '{}/{}/{}'.format(self.domain, self.version, method)
def _build_headers(self, headers=None):
if headers is None:
headers = {}
headers.update({
'X-Auth-Token': 'some-token',
'X-Auth-Token-Test-User-Id': 'some-uid',
})
return headers
To run test suite I have special shell script that performs few actions for me:
#!/usr/bin/env bash
HOST="0.0.0.0"
PORT="5001"
ENVS="PYTHONPATH=$PYTHONPATH:$PWD"
# start server
START_SERVER="$ENVS python $PWD/server.py --port=$PORT --host=$HOST"
eval "$START_SERVER&"
PID=$!
eval "$ENVS nosetests -s --nologcapture --cov-report html --with-cov"
kill -9 $PID
After that view is reported as not being executed.
Ok guys, 12h later I found a solution. I have checked flask, werkzeug, requests, subprocess and thread lib. To only learn that problem is somewhere else. Solution was easy in fact. The bit of code that has to modified is execution of server.py. We need to cover it as well and then merge results generated by server.py and generated by nosetests. Modified test-runner.sh looks as follows:
#!/usr/bin/env bash
HOST="0.0.0.0"
PORT="5001"
ENVS="COVERAGE_PROCESS_START=$PWD/.apirc PYTHONPATH=$PYTHONPATH:$PWD"
START_SERVER="$ENVS coverage run --rcfile=.apirc $PWD/server.py --port=$PORT --host=$HOST"
eval "$START_SERVER&"
eval "$ENVS nosetests -s --nologcapture --cov-config=.apirc --cov-report html --with-cov"
# this is important bit, we have to stop flask server gracefully otherwise
# coverage won't get a chance to collect and save all results
eval "curl -X POST http://$HOST:$PORT/0.0/shutdown/"
# this will merge results from both coverage runs
coverage combine --rcfile=.apirc
coverage html --rcfile=.apirc
Where .apirc in my case is looks as follows:
[run]
branch = True
parallel = True
source = files_to_cover/
[html]
directory = cover
Last thing we need to do is to build into our flask, view that will allow us to greacefully shut down the server. Previously I was brute killing it with kill -9 and it used to kill not only server but coverage as well.
Follow this snippet: http://flask.pocoo.org/snippets/67/
And my view is like that:
def shutdown():
if config.SHUTDOWN_ALLOWED:
func = request.environ.get('werkzeug.server.shutdown')
if func is None:
raise RuntimeError('Not running with the Werkzeug Server')
func()
return 'Server shutting down...'
It is important to use nose-cov instead of standard coverage plugin, as it uses rcfile and allows to configure more. In our case parallel is the key, please note data_files variable is not working for nose-coverage, so you cannot override it in .apirc and you have to use default values.
After all of that, your coverage will be all but shining with valid values.
I hope it going to be helpful for someone out there.