Repository: scrapy/scrapy

Issue:
Contracts for FormRequest
At the moment there's no way to create a contract for a callback that is passed a response from `FormRequest`.

How about something simple like this (`ContractsManager.from_method`):
```python
request = FormRequest(**kwargs) if 'formdata' in kwargs else Request(**kwargs)
```


Discussion:

Resolving patch:
diff --git a/scrapy/contracts/__init__.py b/scrapy/contracts/__init__.py
--- a/scrapy/contracts/__init__.py
+++ b/scrapy/contracts/__init__.py
@@ -49,16 +49,22 @@ def from_spider(self, spider, results):
     def from_method(self, method, results):
         contracts = self.extract_contracts(method)
         if contracts:
+            request_cls = Request
+            for contract in contracts:
+                if contract.request_cls is not None:
+                    request_cls = contract.request_cls
+
             # calculate request args
-            args, kwargs = get_spec(Request.__init__)
+            args, kwargs = get_spec(request_cls.__init__)
             kwargs['callback'] = method
             for contract in contracts:
                 kwargs = contract.adjust_request_args(kwargs)
 
-            # create and prepare request
             args.remove('self')
+
+            # check if all positional arguments are defined in kwargs
             if set(args).issubset(set(kwargs)):
-                request = Request(**kwargs)
+                request = request_cls(**kwargs)
 
                 # execute pre and post hooks in order
                 for contract in reversed(contracts):
@@ -94,6 +100,7 @@ def eb_wrapper(failure):
 
 class Contract(object):
     """ Abstract class for contracts """
+    request_cls = None
 
     def __init__(self, method, *args):
         self.testcase_pre = _create_testcase(method, '@%s pre-hook' % self.name)


Test patch:
diff --git a/tests/test_contracts.py b/tests/test_contracts.py
--- a/tests/test_contracts.py
+++ b/tests/test_contracts.py
@@ -3,11 +3,12 @@
 from twisted.python import failure
 from twisted.trial import unittest
 
+from scrapy import FormRequest
 from scrapy.spidermiddlewares.httperror import HttpError
 from scrapy.spiders import Spider
 from scrapy.http import Request
 from scrapy.item import Item, Field
-from scrapy.contracts import ContractsManager
+from scrapy.contracts import ContractsManager, Contract
 from scrapy.contracts.default import (
     UrlContract,
     ReturnsContract,
@@ -24,6 +25,15 @@ class ResponseMock(object):
     url = 'http://scrapy.org'
 
 
+class CustomFormContract(Contract):
+    name = 'custom_form'
+    request_cls = FormRequest
+
+    def adjust_request_args(self, args):
+        args['formdata'] = {'name': 'scrapy'}
+        return args
+
+
 class TestSpider(Spider):
     name = 'demo_spider'
 
@@ -100,13 +110,20 @@ def parse_no_url(self, response):
         """
         pass
 
+    def custom_form(self, response):
+        """
+        @url http://scrapy.org
+        @custom_form
+        """
+        pass
+
 
 class InheritsTestSpider(TestSpider):
     name = 'inherits_demo_spider'
 
 
 class ContractsManagerTest(unittest.TestCase):
-    contracts = [UrlContract, ReturnsContract, ScrapesContract]
+    contracts = [UrlContract, ReturnsContract, ScrapesContract, CustomFormContract]
 
     def setUp(self):
         self.conman = ContractsManager(self.contracts)
@@ -207,6 +224,12 @@ def test_errback(self):
         self.assertFalse(self.results.failures)
         self.assertTrue(self.results.errors)
 
+    def test_form_contract(self):
+        spider = TestSpider()
+        request = self.conman.from_method(spider.custom_form, self.results)
+        self.assertEqual(request.method, 'POST')
+        self.assertIsInstance(request, FormRequest)
+
     def test_inherited_contracts(self):
         spider = InheritsTestSpider()