I have a Class based view defined as:
class Myview(LoginRequiredMixin, View):
def post():
#.......
to test this view i tried this
class MyViewTest(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.user = User.objects.create_user(
username='jacob', email='soos#i.com', password='vvggtt')
def view_test(self):
# Create an instance of a POST request.
request = self.factory.post('/my-url/')
request.user = self.user
response = MyView(request)
print (response,"**")
self.assertEqual(response.status_code, 200)
But this gives this error.
response = MyView(request)
TypeError: __init__() takes 1 positional argument but 2 were given
I understand why this error is coming (cinstructor of MyView has 2 ars) but how do i remove it? i couldnt get the details on searching.
we can use django test client
from django.test import Client
class MyViewTest(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(
username='jacob', email='soos#i.com', password='vvggtt')
def view_test(self):
# Create an instance of a POST request.
self.client.login(username="jacob", password="vvggtt")
data = {'name': 'test name'}
res = self.client.post('/my-url/', data)
print(res)
self.assertEqual(res.status_code, 200)
From the docs:
# Use this syntax for class-based views.
response = MyView.as_view()(request)
Try
response = MyView(request=request)
There's a section of the Django docs called Testing Class Based Views which addresses this:
In order to test class-based views outside of the request/response cycle you must ensure that they are configured correctly, by calling setup() after instantiation.
So in your case this looks something like:
def view_test(self):
# Create an instance of a POST request.
request = self.factory.post('/my-url/')
request.user = self.user
my_view = MyView()
my_view.setup(request)
response = my_view.post(request)
self.assertEqual(response.status_code, 200)
Related
I try to test mp3 modification (hence PUT). I have the following so far:
client = Client()
with open('my_modified_audio.mp3', 'rb') as fp:
response = client.put(
f"/resource/{resource_id}/",
data={'audio': fp})
However, I get response.status_code == 415 because the serializer line in DRF's ModelViewSet
serializer = self.get_serializer(instance, data=request.data, partial=partial).
fails with
rest_framework.exceptions.UnsupportedMediaType: Unsupported media type "application/octet-stream" in request.
I have tried setting format="multipart", setting the content type to json or form-encoded, nothing helped so far. The Resource model uses FileField:
class Resource(models.Model):
audio = models.FileField(upload_to='uploads')
How can I make this put request work?
I think that the following will work:
The client:
import requests
...
client = Client()
files = [('audio': open('my_modified_audio.mp3', 'rb'))]
url = f"/resource/{resource_id}/"
# response = client.put(url, data=None, files=files)
# You can test it using the `requests` instead of Client()
response = requests.put(url, data=None, files=files)
The serializer:
class AudioSerializer(serializers.Serializer):
""" AudioSerializer """
audio = serializers.FileField(...)
def create(self, validated_data):
...
def update(self, instance, validated_data):
...
The view:
from rest_framework.generics import UpdateAPIView
class AudioView(UpdateAPIView):
...
parser_classes = (FormParser, MultiPartParser)
serializer_class = AudioSerializer
...
Inspired by #athansp's answer, I compared the source code of client.post and client.put and it turns out, put's implementation slightly differs from post, so a workable way of submitting files with put is:
from django.test.client import MULTIPART_CONTENT, encode_multipart, BOUNDARY
client = Client()
with open('my_modified_audio.mp3', 'rb') as fp:
response = client.put(
f"/resource/{resource_id}/",
data=encode_multipart(BOUNDARY, {
'other_field': 'some other data',
'audio': fp,
}),
content_type=MULTIPART_CONTENT
)
Lol.
Title might be a little confusing.
Say I have an APIView with a post method. Inside the post method, I introduced a class that has its own method. In this case, it's a class that deals with uploading to S3, which is something I want to skip when running unittest.
class SomeView(APIView):
def post(self):
# do something here
input1 = some_process(payload_arg1)
input2 = some_other_process(payload_arg2)
uploader = S3Uploader()
s3_response = uploader.upload_with_aux_fxn(input1, input2)
if s3_response['status_code'] == 200:
# do something else
return Response('Good job I did it!', status_code=200)
else:
return Response('noooo you're horrible!', status_code=400)
Real code has different function calls and responses, obviously.
Now I need to mock that uploader and uploader.upload_with_aux_fxn so I don't actually call S3. How do I mock it?
I tried in my test script
from some_place import S3Uploader
class SomeViewTestCase(TestCase):
def setUp(self):
self.client = APIClient()
uploader_mock = S3Uploader()
uploader_mock.upload_support_doc = MagicMock(return_value={'status_code': 200, 'message': 'asdasdad'}
response = self.client.post(url, payload, format='multipart')
But I still triggered S3 upload (as file shows up in S3). How do I correctly mock this?
EDIT1:
My attempt to patch
def setUp(self):
self.factory = APIRequestFactory()
self.view = ViewToTest.as_view()
self.url = reverse('some_url')
#patch('some_place.S3Uploader', FakeUploader)
def test_uplaod(self):
payload = {'some': 'data', 'other': 'stuff'}
request = self.factory.post(self.url, payload, format='json')
force_authenticate(request, user=self.user)
response = self.view(request)
where the FakeUplaoder is
class FakeUplaoder(object):
def __init__(self):
pass
def upload_something(self, data, arg1, arg2, arg3):
return {'status_code': 200, 'message': 'unit test', 's3_path':
'unit/test/path.pdf'}
def downlaod_something(self, s3_path):
return {'status_code': 200, 'message': '', 'body': 'some base64
stuff'}
unfortunately this is not successful. I still hit the actual class
EDIT 2:
I'm using Django 1.11 and Python 2.7, in case people need this info
I guess the correct approach to it would be save the file within a model with FileField, and then connect Boto to handle upload in production scenario.
Take a good look at:
https://docs.djangoproject.com/en/2.2/ref/models/fields/#filefield
and
https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#model
this approach would preserve Django default behavior, making things more testable with Django's default test client.
Take a look at vcrpy. It records request to external API once and then replays answer every time you run your tests. No need to manually mock anything.
Here's an example of how I would mock that S3Uploader in an APITestCase.
from rest_framework import status
from unittest import mock
from unittest.mock import MagicMock
class SomeViewTestCase(APITestCase):
#mock.patch("path.to.view_file.S3Uploader")
def test_upload(self, s3_uploader_mock):
"""Test with mocked S3Uploader"""
concrete_uploader_mock = MagicMock(**{
"upload_with_aux_fxn__return_value": {"status_code": 200}
})
s3_uploader_mock.return_value = concrete_uploader_mock
response = self.client.post(url, payload, format='multipart')
self.assertEqual(response.status_code, status.HTTP_200_OK)
s3_uploader_mock.assert_called_once()
concrete_uploader_mock.upload_with_aux_fx.assert_called_once()
Try using MagicMock like below
from unittest import mock
from storages.backends.s3boto3 import S3Boto3Storage
class SomeTestCase(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.view = ViewToTest.as_view()
self.url = reverse('some_url')
#mock.patch.object(S3Boto3Storage, '_save', MagicMock(return_value='/tmp/somefile.png'))
def test_uplaod(self):
payload = {'some': 'data', 'other': 'stuff'}
request = self.factory.post(self.url, payload, format='json')
force_authenticate(request, user=self.user)
response = self.view(request)
I am using django 1.11.9
I want to add client_id and client_secret to the django POST request.
Here is how my middleware.py file looks like:
class LoginMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
# auth_header = get_authorization_header(request)
# Code to be executed for each request before
# the view (and later middleware) are called.
#Add Django authentication app client data to the request
request.POST = request.POST.copy()
request.POST['client_id'] = '12345678'
request.POST['client_secret'] = '12345678'
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
Middleware is being successfully processed when I check it with a debugger. Thought when a view is called the 'client_id' and 'client_secret' fields are missing in the request.
After some experimenting i figure out that request is not getting updated and when it is called in a different view, it returns old values.
I am later using request in rest_framework_social_oauth2. And this is the point when 'client_id' and 'client_secret' disappear.
class ConvertTokenView(CsrfExemptMixin, OAuthLibMixin, APIView):
"""
Implements an endpoint to convert a provider token to an access token
The endpoint is used in the following flows:
* Authorization code
* Client credentials
"""
server_class = SocialTokenServer
validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS
oauthlib_backend_class = KeepRequestCore
permission_classes = (permissions.AllowAny,)
def post(self, request, *args, **kwargs):
import pdb ; pdb.set_trace()
# Use the rest framework `.data` to fake the post body of the django request.
request._request.POST = request._request.POST.copy()
for key, value in request.data.items():
request._request.POST[key] = value
url, headers, body, status = self.create_token_response(request._request)
response = Response(data=json.loads(body), status=status)
for k, v in headers.items():
response[k] = v
return response
I need to add client_id and client_secret to the request body, so it can be later used by rest_framework_social_oauth2.
What could be the problem? How to properly update the request?
As you're working with request and processing a request, you have to implement process_request method, so the result will be something like:
class LoginMiddleware(object):
def process_request(self, request):
request.session['client_id'] = '12345678'
and then in your view:
def your_view(request):
client_id = request.session['client_id']
I try to test view that has custom authentication, mainly because the main auth is based on external login-logout system, utilizing Redis as db for storing sessions.
Auth class is checking session id from the request, whether it is the same as in Redis - if yes, succeed.
My custom authentication.py looks like:
from django.utils.six import BytesIO
from rest_framework import authentication
from rest_framework import exceptions
from rest_framework.parsers import JSONParser
import redis
class RedisAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
print(request.META)
token = request.META['HTTP_X_AUTH_TOKEN']
redis_host = "REDIS_IP_ADRESS"
redis_db = redis.StrictRedis(host=redis_host)
user_data = redis_db.get("user_feature:{}".format(token))
if user_data is None:
raise exceptions.AuthenticationFailed('No such user or session expired')
try:
stream = BytesIO(user_data) # Decode byte type
data = JSONParser(stream) # Parse bytes class and return dict
current_user_id = data['currentUserId']
request.session['user_id'] = current_user_id
except Exception as e:
print(e)
return (user_data, None)
and my views.py looks like:
#api_view(['GET', 'POST'])
#authentication_classes((RedisAuthentication, ))
def task_list(request):
if request.method == 'GET':
paginator = PageNumberPagination()
task_list = Task.objects.all()
result_page = paginator.paginate_queryset(task_list, request)
serializer = TaskSerializer(result_page, many=True)
return paginator.get_paginated_response(serializer.data)
elif request.method == 'POST':
serializer = PostTaskSerializer(data=request.data)
if serializer.is_valid():
user_id = request.session.get('user_id')
serializer.save(owner_id=user_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Manual tests pass, but my current pytests failed after adding authentication.py, and have no clue how I can fix it properly - tried with forcing auth, but no succeed.
I'm thinking that one of solution will be use fakeredis for simulate real redis. Question is, how that kind of test should looks like?
Example of test you could find here:
#pytest.mark.webtest
class TestListView(TestCase):
def setUp(self):
self.client = APIClient()
def test_view_url_accessible_by_name(self):
response = self.client.get(
reverse('task_list')
)
assert response.status_code == status.HTTP_200_OK
#pytest.mark.webtest
class TestCreateTask(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(username='admin', email='xx', password='xx')
def test_create(self):
data = {some_data}
self.client.login(username='xx', password='xx')
response = self.client.post(
reverse('task_list'),
data,
format='json')
assert response.status_code == status.HTTP_201_CREATED
self.client.logout()
Thanks in advance for any help!
I managed to mock whole redis auth using mock.patch decorator - https://docs.python.org/3.5/library/unittest.mock-examples.html#patch-decorators.
When you put import patch to mock.patch decorator, do not insert absolute module path where redis code is stored, but insert the path where redis code was imported as a module and used.
My test looks like that now:
#mock.patch('api.views.RedisAuthentication.authenticate')
def test_view_url_accessible_by_name(self, mock_redis_auth):
data = {"foo": 1, "currentUserId": 2, "bar": 3}
mock_redis_auth.return_value = (data, None)
response = self.client.get(
reverse('task_list'),
HTTP_X_AUTH_TOKEN='foo'
)
assert response.status_code == status.HTTP_200_OK
I have many django forms in which I pass the request as kwarg.
I've just started dive into testing, and it seems that testing forms which require the request as argument makes the testing harder. As I have to somehow create a request and I cant test my forms without it.
So is it best to avoid passing the request to the form at all? Or another workaround?
The reason I do that on first place is that sometimes I need request.user, or request.session and do some cleaning/setting based on that info in the form.
UPDATE:
This is an example form:
class OrderForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request')
self.user = self.request.user
def clean(self):
# Here I have some cross session-field validation
if self.request.session['has_response'] and self.cleaned_data('status') == 'NEW':
raise ValidationError()
def save(self, commit=False):
self.instance.user = self.user
return super(OrderForm, self).save(commit=True)
class Meta:
model = Order
fields = ('address', 'city', 'status', ) # more fields
The view code is simple:
form = OrderForm(request.POST, request=request)
The Order model also have a clean() method with some validation logic.
The session is populated at most during the user login.
The point it I need the session/user there.
But most important, the question - is it a bad design to pass the request and session to the form, considering options for testing this form? I find it more logical when the form take care for saving the object, including the request.user. But maybe I should try to split that between the form and view?
Passing request to the form is okay if you need it in the clean() method. You can use a request/session/user in a test like this:
from django.test import TestCase, Client
from django.test.client import RequestFactory
from django.contrib.auth.models import AnonymousUser, User
from .views import my_view
from .forms import MyForm
from django.contrib.sessions.middleware import SessionMiddleware
# If Python >= 3.4
from unittest.mock import patch, MagicMock
# Else
from mock import patch, MagicMock
class SimpleTest(TestCase):
def setUp(self):
# Create a RequestFactory accessible by the entire class.
self.factory = RequestFactory()
# Create a new user object accessible by the entire class.
self.user = User.objects.create_user(username='username',
email='email', password='password')
def test_my_view(self):
# Create an instance of a GET request.
request = self.factory.get('/my-url/')
# Middleware is not supported so simulate a
# logged-in user by setting request.user.
request.user = self.user
# Or add anonymous user to request.
request.user = AnonymousUser()
# Test view() at '/my-url/'
response = my_view(request)
self.assertEqual(response.status_code, 200)
#patch('app.models.ModelName.save', MagicMock(name="save"))
def test_my_form_view_with_factory(self):
# Set up form data.
form_data = {'something': 'something'}
# Create an instance of a POST request.
request = self.factory.post('/my-form-url/', form_data)
# Simulate logged-in user
request.user = self.user
# Setup session.
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
# Or you should just be able to do
request.session['somekey'] = 'test'
request.session.save()
# Get response from form view, and test passing
# request/data to form.
form = MyForm(request=request, data=form_data)
response = my_form_view(request)
self.assertTrue(form.is_valid())
self.assertEqual(response.status_code, 200)
# If model form you can do
self.assertTrue(ModelName.save.called)
#patch('app.models.ModelName.save', MagicMock(name="save"))
def test_my_form_view_with_client(self):
# Use Client instead of RequestFactory.
self.client = Client()
# Login with Client.
self.client.login(username='username', password='password')
# Set up form data.
form_data = {'something': 'something'}
# Get/set session.
session = self.client.session
session['somekey'] = 'test'
session.save()
# Get response with Client.
response = self.client.post('/my-form-url/', form_data)
self.assertEqual(response.status_code, 200)
# If model form you can do
self.assertTrue(ModelName.save.called)
Should give a general idea of what you can do, not specifically tested.