17

I would like to unit test a django view by sumitting a form. The problem is that this form has a captcha field (based on django-simple-captcha).

from django import forms
from captcha.fields import CaptchaField

class ContactForm(forms.forms.Form):
    """
    The information needed for being able to download
    """
    lastname = forms.CharField(max_length=30, label='Last name')
    firstname = forms.CharField(max_length=30, label='First name')
    ...
    captcha = CaptchaField()

The test code:

class ContactFormTest(TestCase):

    def test_submitform(self):
        """Test that the contact page"""
        url = reverse('contact_form')

        form_data = {}
        form_data['firstname'] = 'Paul'
        form_data['lastname'] = 'Macca'
        form_data['captcha'] = '28if'

        response = self.client.post(url, form_data, follow=True)

Is there any approach to unit-test this code and get rid of the captcha when testing?

Thanks in advance

luc
  • 41,928
  • 25
  • 127
  • 172
  • 1
    In case others end up here like I did, I stumbled on this post trying to find a similar answer for the `django-recaptcha` package; turns out they also have a setting. Their docs describe its use: https://github.com/praekelt/django-recaptcha – Dolan Antenucci Mar 23 '16 at 14:43
  • 1
    For those using django-recaptcha and need to do a post in your unittest, you also need to send "g-recaptcha-response" like this: self.client.post(url, {"g-recaptcha-response": "PASSED"}) – MrValdez May 25 '20 at 07:20

7 Answers7

22

I know this is an old post, but django-simple-captcha now has a setting CAPTCHA_TEST_MODE which makes the captcha succeed if you supply the value 'PASSED'. You just have to make sure to send something for both of the captcha input fields:

post_data['captcha_0'] = 'dummy-value'
post_data['captcha_1'] = 'PASSED'
self.client.post(url, data=post_data)

The CAPTCHA_TEST_MODE setting should only be used during tests. My settings.py:

if 'test' in sys.argv:
    CAPTCHA_TEST_MODE = True 
Vinod Kurup
  • 2,676
  • 1
  • 23
  • 18
  • 1
    Nowadays it would be also possible to use `@override_settings(CAPTCHA_TEST_MODE=True)` from `from django.test import override_settings`; but unfortunately as of February 2019 there is an issue that this very setting is read only once - when the application starts. See https://github.com/mbi/django-simple-captcha/issues/84 – Jack L. Feb 20 '19 at 11:16
7

Here's the way I got around it. Import the model that actually holds Captcha info:

from captcha.models import CaptchaStore

First, I check that the test captcha table is empty:

captcha_count = CaptchaStore.objects.count()
self.failUnlessEqual(captcha_count, 0)

After loading the page (in this case, it's a registration page), check that there's a new captcha object instance:

captcha_count = CaptchaStore.objects.count()
self.failUnlessEqual(captcha_count, 1)

Then, I retrieve the captcha instance data and POST that with the form. In my case, the POST expects 'captcha_0' to contain the hashkey, and 'captcha_1' to contain the response.

captcha = CaptchaStore.objects.all()[0]
registration_data = { # other registration data here
                     'captcha_0': captcha.hashkey,
                     'captcha_1': captcha.response }

You may need to tweak this a little if you start with CaptchaStore instances before you run this test. Hope that helps.

Jim McGaw
  • 768
  • 6
  • 13
  • The way I did (before noticing your answer) was to parse the unbound form HTML `dom = PyQuery('{}'.format(f.as_p())`, get the hash from there `hashkey = dom('input[name="captcha_0"]').attr('value')` and then query the database using it. The rest is mostly the same. Hope it hopes someone. – alfetopito Jul 02 '13 at 16:20
4

I unit tested it by mocking the ReCaptchaField. First, I've added the recaptcha field in the constructor. It cannot be added as a regular field because you won't be able to mock it (once the code is evaluated before the mock is being applied):

class MyForm(forms.ModelForm):

    ...

    def __init__(self, *args, **kwargs):
        # Add captcha in the constructor to allow mock it
        self.fields["captcha"] = ReCaptchaField()

Then, I just replaced the ReCaptchaField by a not required CharField. This way, I'm trusting django-recaptcha will work. I can test only my own stuff:

@mock.patch("trials.forms.ReCaptchaField", lambda: CharField(required=False))
def test_my_stuff(self):
    response = self.client.post(self.url, data_without_captcha)
    self.assert_my_response_fit_the_needs(response)
Michel Sabchuk
  • 810
  • 7
  • 12
  • To save anyone else as forgetful as me from needing to look them up, you'll need: `from django.db.models import CharField` and `from unittest import mock`. – Phil Gyford May 29 '21 at 14:13
3

Here is how we do it.

@patch("captcha.fields.ReCaptchaField.validate")
def test_contact_view(self, validate_method):

    response = self.client.get(reverse("contact"))
    self.assertEqual(response.status_code, 200)

    data = {
        "name": "Bob Johnson",
        "email": "big_johnson@home.com",
        "phone": "800-212-2001",
        "subject": "I want Axis!",
        "message": "This is a giant\nThree liner..\nLove ya\n",
        "captcha": "XXX",
    }
    validate_method.return_value = True
    response = self.client.post(reverse("contact"), data=data)

    self.assertEqual(response.status_code, 302)
rh0dium
  • 6,811
  • 4
  • 46
  • 79
1

One solution is have a setting "testing" that is either true or false. And then just

if not testing:
   # do captcha stuff here

It's simple and easy, and an easy toggle.

Wayne Werner
  • 49,299
  • 29
  • 200
  • 290
  • It works but the settings.UNIT_TEST = True must be set before importing the form in the test module. That was the cause of my mistake – luc Jul 01 '10 at 16:01
  • 1
    you can set testing in the settings file too: `if "test" in sys.argv: TESTING = True` – leech Oct 30 '12 at 19:26
1

Another solutions which is similar to Jim McGaw's answer but remove the need of empty table CaptchaStore table.

captcha = CaptchaStore.objects.get(hashkey=CaptchaStore.generate_key())

registration_data = { # other registration data here
                 'captcha_0': captcha.hashkey,
                 'captcha_1': captcha.response }

This will generate new captcha just for that test.

VStoykov
  • 1,705
  • 12
  • 15
0

With a similar approach than Jim McGaw but using BeautifulSoup:

from captcha.models import CaptchaStore
from BeautifulSoup import BeautifulSoup

data = {...} #The data to post
soup = BeautifulSoup(self.client.get(url).content)
for field_name in ('captcha_0', ...): #get fields from the form
    data[field_name] = soup.find('input',{'name':field_name})['value']
captcha = CaptchaStore.objects.get(hashkey=data['captcha_0'])
data['captcha_1'] = captcha.challenge
response = self.client.post(url, data=data)

# check the results
...
luc
  • 41,928
  • 25
  • 127
  • 172