i just learning how to pytest my code in django, and i need your help. So, i have tested my forms.py already,and now i want to test my views.py. I know that i need to test is it post on page,like by response on by ORM, but i cant understand how to do that, probably with my factories or no?
This is my views.py
class AddPost(CreateView):
model = Posts
form_class = PostsForm
template_name = 'posts/addpost.html'
success_url = '/'
def form_valid(self, form):
instance = form.save(commit=False)
if self.request.user.is_authenticated:
instance.owner = self.request.user
instance.save()
return HttpResponseRedirect(self.get_success_url())
class ShowPost(ListView):
model = Posts
template_name = 'posts/allposts.html'
paginate_by = 2
this is test_forms
#pytest.mark.django_db(True)
class TestPostCreationForm:
def test_form(self):
proto_post = PostsFactory.build()
form_payload = {
'phone_number': proto_post.phone_number,
'title': proto_post.title,
'type': proto_post.type,
'text': proto_post.text,
'price': proto_post.price,
'status': proto_post.status,
'image': proto_post.image,
}
form = PostsForm(form_payload)
assert form.is_valid()
instance = form.save()
assert instance.phone_number == proto_post.phone_number
assert instance.title == proto_post.title
assert instance.price == proto_post.price
and factories
from users.tests.factories import UserFactory
def get_mock_img(name='test.png', ext='png', size=(50, 50), color=(256, 0, 0)):
file_obj = BytesIO()
image = Image.new("RGB", size=size, color=color)
image.save(file_obj, ext)
file_obj.seek(0)
return File(file_obj, name=name)
class PostsFactory(factory.DjangoModelFactory):
owner = SubFactory(UserFactory)
phone_number = factory.Faker("phone_number", locale='uk_UA')
title = factory.fuzzy.FuzzyText(length=50)
text = factory.fuzzy.FuzzyText(length=250)
price = factory.fuzzy.FuzzyDecimal(10.5, 50.5)
status = factory.fuzzy.FuzzyChoice(choices=['active', 'deactivated'])
type = factory.fuzzy.FuzzyChoice(choices=['private', 'business'])
image = get_mock_img()
class Meta:
model = 'posts.Posts'
Use the Django test client. Although I don't use pytest, so I can't be certain that it's completely separable from the Django test runner which I use. I'd be surprised if that was not the case
Typical usage (POST to a simple form, which is expected to succeed and redirect)
data = {
"number_per_gridbox": "10",
"submit": "Submit" }
url_fill_gridbox = reverse( "jobs:qa_break_into_gridboxes",
kwargs={'jobline': self.jobline.pk ,'stockline': self.stockline.pk})
response = self.client.post( url_gridbox, data)
self.assertRedirects( response, url_fill_gridbox)
where you are testing error responses or GET, you can obtain the rendered HTML with
body = response.content.decode()
and you can look at entities in the context data that was used for rendering:
form_errors = response.context['form'].errors
As for setting up some objects for the view to process, you can either just create and save them with code (often in the setUp method), or you can use faker and factory-boy
Related
I do coverage report and there are many place that not cover, do you have any idea how to I write test for th?
this views.py was not cover
def addreview(request, u_id, shop_id):
url = request.META.get('HTTP_REFERER')
shop = shop_detail.objects.get(id=shop_id)
user = Profile.objects.get(customer=u_id)
if request.method == 'POST':
form = ReviewForm(request.POST)
if form.is_valid():
data = Review()
data.review_text = form.cleaned_data['review_text']
data.review_rating = form.cleaned_data['review_rating']
data.shop = shop
data.user = user
data.save()
return redirect(url)
def rating(request):
if not request.user.is_authenticated:
return HttpResponseRedirect(reverse('customer_login'))
if request.method == 'POST':
form = RateUsForm(request.POST)
if form.is_valid():
rate = RateUs()
rate.rate_text = form.cleaned_data['rate_text']
rate.rating = form.cleaned_data['rating']
rate.user = request.user
rate.save()
return HttpResponseRedirect(reverse("index"))
return render(request, 'shop/rate.html')
models.py
class Review(models.Model):
user = models.ForeignKey(Profile,on_delete=models.CASCADE)
shop = models.ForeignKey(shop_detail,on_delete=models.CASCADE)
review_text = models.TextField(max_length=300)
review_rating = models.IntegerField()
def __str__(self):
return f"{self.review_rating}"
class RateUs(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
rate_text = models.TextField()
rating = models.IntegerField()
def __str__(self):
return f"{self.rating}"
forms.py
class ReviewForm(forms.ModelForm):
class Meta:
model= Review
fields= ["review_text", "review_rating"]
class RateUsForm(forms.ModelForm):
class Meta:
model= RateUs
fields= ["rate_text","rating"]
This is what I write for rating
def test_valid_rating(self):
rateus1 = RateUs.objects.first()
data={
"user": rateus1.user,
"rate_text": rateus1.rate_text,
"rating": rateus1.rating
}
response = self.client.post(reverse('rating'), data=data)
self.assertEqual(response.status_code, 302)
When I try to write test for addreview i got customer.models.Profile.DoesNotExist: Profile matching query does not exist.
Here is what you need to implement to get a way to test your function:
# Imports needed
import pytest
from unittest.mock import MagicMock, patch
def test_addreview(self):
# Here probably you will need to create a User before.
user = User.objects.create(username='testuser', id=15445)
shop = shop_detail.objects.create(id=19889)
Profile.objects.create(customer=user.pk)
mock_request = MagicMock(
POST={'review_text': 'test', 'review_rating': 5},
META={'HTTP_REFERER': 'http://testserver/'},
method='POST',
)
self.assert(Review.objects.count(), 0)
with patch('django.shortcuts.redirect') as redirect_mock:
addreview(mock_request, user.pk, shop.pk)
redirect_mock.assert_called_with('http://testserver/')
self.assert(Review.objects.count(), 1)
def test_addreview_not_redirected(self):
# Here probably you will need to create a User before.
user = User.objects.create(username='testuser', id=15445)
shop = shop_detail.objects.create(id=19889)
Profile.objects.create(customer=user.pk)
mock_request = MagicMock(
POST={'review_text': 'test', 'review_rating': 5},
META={'HTTP_REFERER': 'http://testserver/'},
method='GET',
)
self.assert(Review.objects.count(), 0)
with patch('django.shortcuts.redirect') as redirect_mock:
addreview(mock_request, user.pk, shop.pk)
redirect_mock.assert_not_called()
self.assert(Review.objects.count(), 0)
def test_addreview_raise_exception_profile_not_found(self):
# Here probably you will need to create a User before.
user = User.objects.create(username='testuser', id=15445)
shop = shop_detail.objects.create(id=19889)
mock_request = MagicMock(
POST={'review_text': 'test', 'review_rating': 5},
META={'HTTP_REFERER': 'http://testserver/'},
method='GET',
)
self.assert(Review.objects.count(), 0)
self.assert(Profile.objects.count(), 0)
with pytest.raises(Profile.DoesNotExist):
addreview(mock_request, user.pk, shop.pk)
self.assert(Review.objects.count(), 0)
self.assert(Profile.objects.count(), 0)
Probaly you will need to adapt a little bit each example as it is just a guide of how you have to implement it.
When working with this kind of test if you expect for example a Profile to exists you need to create it before to anticipate to the lines of code that will be executed once the function is called.
Mock are so important in this case we mock request and simulate all the properties that a request has.
We also mock redirect but in this case just to knwo if it was called or not.
At the end we generate a case where what we expect is the code to generate an exception when calling the funcion.
Normally to create model objects in test the best practice is to use https://factoryboy.readthedocs.io/en/stable/ take a look of that.
This question is directly related to this question, but that one is now outdated it seems.
I am trying to test a view without having to access the database. To do that I need to Mock a RelatedManager on the user.
I am using pytest and pytest-mock.
models.py
# truncated for brevity, taken from django-rest-knox
class AuthToken(models.Model):
user = models.ForeignKey(
User,
null=False,
blank=False,
related_name='auth_token_set',
on_delete=models.CASCADE
)
views.py
class ChangeEmail(APIView):
permission_classes = [permissions.IsAdmin]
serializer_class = serializers.ChangeEmail
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = request.user
user.email = request.validated_data['email']
user.save()
# Logout user from all devices
user.auth_token_set.all().delete() # <--- How do I mock this?
return Response(status=status.HTTP_200_OK)
test_views.py
def test_valid(mocker, user_factory):
user = user_factory.build()
user.id = 1
data = {
'email': 'foo#example.com'
}
factory = APIRequestFactory()
request = factory.post('/', data=data)
force_authenticate(request, user)
mocker.patch.object(user, "save")
related_manager = mocker.patch(
'django.db.models.fields.related.ReverseManyToOneDescriptor.__set__',
return_vaue=mocker.MagicMock()
)
related_manager.all = mocker.MagicMock()
related_manager.all.delete = mocker.MagicMock()
response = ChangeEmail.as_view()(request)
assert response.status_code == status.HTTP_200_OK
Drawing from the answer in the linked question I tried to patch the ReverseManyToOneDescriptor. However, it does not appear to actually get mocked because the test is still trying to connect to the database when it tries to delete the user's auth_token_set.
You'll need to mock the return value of the create_reverse_many_to_one_manager factory function. Example:
def test_valid(mocker):
mgr = mocker.MagicMock()
mocker.patch(
'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager',
return_value=mgr
)
user = user_factory.build()
user.id = 1
...
mgr.assert_called()
Beware that the above example will mock the rev manager for all models. If you need a more fine-grained approach (e.g. patch User.auth_token's rev manager only, leave the rest unpatched), provide a custom factory impl, e.g.
def test_valid(mocker):
mgr = mocker.MagicMock()
factory_orig = related_descriptors.create_reverse_many_to_one_manager
def my_factory(superclass, rel):
if rel.model == User and rel.name == 'auth_token_set':
return mgr
else:
return factory_orig(superclass, rel)
mocker.patch(
'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager',
my_factory
)
user = user_factory.build()
user.id = 1
...
mgr.assert_called()
I accomplish this doing this(Django 1.11.5)
#patch("django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager")
def test_reverse_mock_count(self, reverse_mock):
instance = mommy.make(DjangoModel)
manager_mock = MagicMock
count_mock = MagicMock()
manager_mock.count = count_mock()
reverse_mock.return_value = manager_mock
instance.related_manager.count()
self.assertTrue(count_mock.called)
hope this help!
If you use django's APITestCase, this becomes relatively simple.
class TestChangeEmail(APITestCase):
def test_valid(self):
user = UserFactory()
auth_token = AuthToken.objects.create(user=user)
response = self.client.post(
reverse('your endpoint'),
data={'email': 'foo#example.com'}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(AuthToken.objects.filter(user=user).exists())
This avoids mocking altogether and gives a more accurate representation of your logic.
unittest.PropertyMock can be used to mock descriptors in a way that doesn't require mocking internal implementation details:
def test_valid(mocker, user_factory):
user = user_factory.build()
user.id = 1
data = {
'email': 'foo#example.com'
}
factory = APIRequestFactory()
request = factory.post('/', data=data)
force_authenticate(request, user)
mocker.patch.object(user, "save")
with mocker.patch('app.views.User.auth_token_set', new_callable=PropertyMock) as mock_auth_token_set:
mock_delete = mocker.MagicMock()
mock_auth_token_set.return_value.all.return_value.delete = mock_delete
response = ChangeEmail.as_view()(request)
assert response.status_code == status.HTTP_200_OK
assert mock_delete.call_count == 1
Working with another coder on a project. His change stopped the UpdateView on some forms from saving edits. I realized why.... he defined a
def post
which works for the case he was working on, but needs an else action that just does a default update. I am not sure how to do this when he UpdateView isn't doing it all automagically.
The code to the UpdateView:
class ProviderUpdateView(UpdateView):
model = Provider
form_class = ProviderForm
provider_form_class = ProviderForm
provider_term_form_class = ProviderTermForm
template_name = 'ipaswdb/provider/provider_form.html'
success_url = '/ipaswdb/provider/'
def get_context_data(self, **kwargs):
context = super(ProviderUpdateView, self).get_context_data(**kwargs)
provider = context['object']
context['provider_id'] = provider.id
prov = Provider.objects.get(pk=provider.id)
#print "provider: ",
#print prov
#print "provider terminated: ",
#print prov.is_terminated
if prov.is_terminated:
provider_form = ProviderFormView(instance=prov)
context['readonly'] = True
else:
print("NOT TERMINATED SO LETS DO THIS")
provider_form = ProviderForm(instance=prov)
context['readonly'] = False
context['provider_form'] = provider_form
provider_term_form = ProviderTermForm()
context['provider_term_form'] = provider_term_form
### Get the list of GroupLocations, but remove the current one this provider is associated with
### I just need the grouplocation id and the name
#grplocs = GroupLocationProvider.objects.filter(
return context
def post(self, request, *args, **kwargs):
#print self.request.POST.keys()
#print self.request.POST.values()
print("Posting...")
if self.request.POST.has_key('terminate'):
provider = Provider.objects.get(pk=kwargs['pk'])
form = ProviderTermForm(request.POST)
if form.is_valid():
print "Terminating Provider: ",
print provider
provider_term = ProviderTerm()
provider.is_terminated = True
provider.save()
### Update the term fields
provider_term.provider = provider
provider_term.old_id = provider.id
provider_term.term_date = form.cleaned_data['term_date']
provider_term.term_comment = form.cleaned_data['term_comment']
provider_term.save()
return HttpResponseRedirect(self.success_url)
I know I need an else to this statement in the post:
if self.request.POST.has_key('terminate'):
I am just not sure what the just do your regular thing' is in the UpdateView. I tested my hypothesis that his code broke the ability to edit and save a provider cause I removed the def post completely, and all worked well with the UpdateView automagic. Since we are overriding? the def post it seems to me we have to handle the regular update ourselves, just not sure how that looks.
On my scorecard entry form, i only want the user to select from the shortlisted players for that match. If there was one field, I am succesfully able to rewrite using.
form.fields['player'].queryset = PlayerShortlist.objects.filter(team=userteam, fixture=fixture_id)
but when i apply it on formset_factory, i am unable to get result.
my forms.py
class TossForm(forms.Form):
toss_won_by = forms.BooleanField()
bat_first = forms.BooleanField()
class InningsForm(forms.Form):
player = forms.ModelChoiceField(
PlayerShortlist.objects.all()
)
status = forms.ChoiceField(choices=OUT_CHOICES, initial='DNB')
score = forms.IntegerField(initial=0)
balls_faced = forms.IntegerField(initial=0)
my views.py
#login_required
def scorecard(request, team_id, fixture_id):
template = get_template('cricket/scorecard.html')
tossform = TossForm()
#inningform = InningsForm()
InningsForms = formset_factory(InningsForm, extra=11)
inningsforms = InningsForm()
inningsforms.fields['player'].queryset = PlayerShortlist.objects.filter(team=Team.objects.get(id=1), fixture=fixture_id)
page_vars = Context({
'loggedinuser': request.user,
'tossform': tossform,
'inningsforms': inningsforms,
})
crsfcontext = RequestContext(request, page_vars)
output = template.render(crsfcontext)
return HttpResponse(output)
it gives me errors.
'InningsFormFormSet' object has no attribute 'fields'
thanks
//yousuf
okey, i looked around, and it seems formfield_callback can be used for what i intend it for but when i use it lin my views.py like
def update_field(field, **kwargs):
if field.name == 'players':
field.queryset = PlayerShortlist.objects.filter(team=Team.objects.get(id=team_id), fixture=fixture_id)
InningsFormset = formset_factory(InningsForm, extra=11, formfield_callback)
it gives me
formset_factory() got an unexpected keyword argument 'formfield_callback'
Remember: a formset wraps around a list of forms. So this:
inningsforms.fields['player'].queryset = PlayerShortlist.objects.filter(team=Team.objects.get(id=1), fixture=fixture_id)
Should rather be:
qs = PlayerShortlist.objects.filter(team=Team.objects.get(id=1), fixture=fixture_id)
# force execution of the queryset once and for all
list(qs)
for form in inningsforms.forms:
form.fields['player'].queryset = qs
Also, formfield_callback is an argument of modelformset_factory (and modelform_factory), not of formset_factory. See how it is used:
def modelformset_factory(model, form=ModelForm, formfield_callback=None,
formset=BaseModelFormSet,
extra=1, can_delete=False, can_order=False,
max_num=None, fields=None, exclude=None):
"""
Returns a FormSet class for the given Django model class.
"""
form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
formfield_callback=formfield_callback)
FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
can_order=can_order, can_delete=can_delete)
FormSet.model = model
return FormSet
See, formfield_callback is proxied to modelform_factory by modelformset_factory.
I've tried to import a csv file into a database by tweaking the modelform inside the admin doing this:
models.py:
class Data(models.Model):
place = models.ForeignKey(Places)
time = models.DateTimeField()
data_1 = models.DecimalField(max_digits=3, decimal_places=1)
data_2 = models.DecimalField(max_digits=3, decimal_places=1)
data_3 = models.DecimalField(max_digits=4, decimal_places=1)
Forms.py:
import csv
class DataImport(ModelForm):
file_to_import = forms.FileField()
class Meta:
model = Data
fields = ("file_to_import", "place")
def save(self, commit=False, *args, **kwargs):
form_input = DataImport()
self.place = self.cleaned_data['place']
file_csv = request.FILES['file_to_import']
datafile = open(file_csv, 'rb')
records = csv.reader(datafile)
for line in records:
self.time = line[1]
self.data_1 = line[2]
self.data_2 = line[3]
self.data_3 = line[4]
form_input.save()
datafile.close()
Admin.py:
class DataAdmin(admin.ModelAdmin):
list_display = ("place", "time")
form = DataImport
admin.site.register(Data, DataAdmin)
But i'm stuck trying to import the file i put in "file_to_import" field. Getting AttributeError in forms.py : 'function' object has no attribute 'FILES'.
What i'm doing wrong?
After a long search i found an answer: Create a view inside the admin using a standard form
Form:
class DataInput(forms.Form):
file = forms.FileField()
place = forms.ModelChoiceField(queryset=Place.objects.all())
def save(self):
records = csv.reader(self.cleaned_data["file"])
for line in records:
input_data = Data()
input_data.place = self.cleaned_data["place"]
input_data.time = datetime.strptime(line[1], "%m/%d/%y %H:%M:%S")
input_data.data_1 = line[2]
input_data.data_2 = line[3]
input_data.data_3 = line[4]
input_data.save()
The view:
#staff_member_required
def import(request):
if request.method == "POST":
form = DataInput(request.POST, request.FILES)
if form.is_valid():
form.save()
success = True
context = {"form": form, "success": success}
return render_to_response("imported.html", context,
context_instance=RequestContext(request))
else:
form = DataInput()
context = {"form": form}
return render_to_response("imported.html", context,
context_instance=RequestContext(request))
The rest is part of this post:
http://web.archive.org/web/20100605043304/http://www.beardygeek.com/2010/03/adding-views-to-the-django-admin/
Take a look at django-admin-import, it does more or less exactly what you want -- you can upload a XLS (not a CSV, but that should not matter) and lets you assign columns to model fields. Default values are also supported.
https://pypi.org/project/django-admin-import/
Additionally, it does not take away the possibility to modify individual records by hand because you don't have to replace the default model form used in the administration.
In the save() method, you don't have any access to the request object - you can see that it's not passed in. Normally you would expect to have a NameError there, but I suspect that you've got a function elsewhere in the file called request().
At the point of saving, all the relevant data should be in cleaned_data: so you should be able to do
file_csv = self.cleaned_data['file_to_import']
At that point you'll have another problem, which is when you get to open - you can't do that, as file_to_import is not a file on the server filesystem, it's an in-memory file that has been streamed from the client. You should be able to pass file_csv directly to csv.reader.