151 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			ReStructuredText
		
	
	
	
			
		
		
	
	
			151 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			ReStructuredText
		
	
	
	
| :orphan:
 | |
| 
 | |
| =========================
 | |
| Parametrize with fixtures
 | |
| =========================
 | |
| 
 | |
| Problem
 | |
| -------
 | |
| 
 | |
| As a user I have functional tests that I would like to run against various
 | |
| scenarios.
 | |
| 
 | |
| In this particular example we want to generate a new project based on a
 | |
| cookiecutter template. We want to test default values but also data that
 | |
| emulates user input.
 | |
| 
 | |
| - use default values
 | |
| 
 | |
| - emulate user input
 | |
| 
 | |
|   - specify 'author'
 | |
| 
 | |
|   - specify 'project_slug'
 | |
| 
 | |
|   - specify 'author' and 'project_slug'
 | |
| 
 | |
| This is how a functional test could look like:
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
|     import pytest
 | |
| 
 | |
|     @pytest.fixture
 | |
|     def default_context():
 | |
|         return {'extra_context': {}}
 | |
| 
 | |
| 
 | |
|     @pytest.fixture(params=[
 | |
|         {'author': 'alice'},
 | |
|         {'project_slug': 'helloworld'},
 | |
|         {'author': 'bob', 'project_slug': 'foobar'},
 | |
|     ])
 | |
|     def extra_context(request):
 | |
|         return {'extra_context': request.param}
 | |
| 
 | |
| 
 | |
|     @pytest.fixture(params=['default', 'extra'])
 | |
|     def context(request):
 | |
|         if request.param == 'default':
 | |
|             return request.getfuncargvalue('default_context')
 | |
|         else:
 | |
|             return request.getfuncargvalue('extra_context')
 | |
| 
 | |
| 
 | |
|     def test_generate_project(cookies, context):
 | |
|         """Call the cookiecutter API to generate a new project from a
 | |
|         template.
 | |
|         """
 | |
|         result = cookies.bake(extra_context=context)
 | |
| 
 | |
|         assert result.exit_code == 0
 | |
|         assert result.exception is None
 | |
|         assert result.project.isdir()
 | |
| 
 | |
| 
 | |
| Issues
 | |
| ------
 | |
| 
 | |
| * By using ``request.getfuncargvalue()`` we rely on actual fixture function
 | |
|   execution to know what fixtures are involved, due to it's dynamic nature
 | |
| * More importantly, ``request.getfuncargvalue()`` cannot be combined with
 | |
|   parametrized fixtures, such as ``extra_context``
 | |
| * This is very inconvenient if you wish to extend an existing test suite by
 | |
|   certain parameters for fixtures that are already used by tests
 | |
| 
 | |
| pytest version 3.0 reports an error if you try to run above code::
 | |
| 
 | |
|     Failed: The requested fixture has no parameter defined for the current
 | |
|     test.
 | |
| 
 | |
|     Requested fixture 'extra_context'
 | |
| 
 | |
| 
 | |
| Proposed solution
 | |
| -----------------
 | |
| 
 | |
| A new function that can be used in modules can be used to dynamically define
 | |
| fixtures from existing ones.
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
|     pytest.define_combined_fixture(
 | |
|         name='context',
 | |
|         fixtures=['default_context', 'extra_context'],
 | |
|     )
 | |
| 
 | |
| The new fixture ``context`` inherits the scope from the used fixtures and yield
 | |
| the following values.
 | |
| 
 | |
| - ``{}``
 | |
| 
 | |
| - ``{'author': 'alice'}``
 | |
| 
 | |
| - ``{'project_slug': 'helloworld'}``
 | |
| 
 | |
| - ``{'author': 'bob', 'project_slug': 'foobar'}``
 | |
| 
 | |
| Alternative approach
 | |
| --------------------
 | |
| 
 | |
| A new helper function named ``fixture_request`` tells pytest to yield all
 | |
| parameters of a fixture.
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
|     @pytest.fixture(params=[
 | |
|         pytest.fixture_request('default_context'),
 | |
|         pytest.fixture_request('extra_context'),
 | |
|     ])
 | |
|     def context(request):
 | |
|         """Returns all values for ``default_context``, one-by-one before it
 | |
|         does the same for ``extra_context``.
 | |
| 
 | |
|         request.param:
 | |
|             - {}
 | |
|             - {'author': 'alice'}
 | |
|             - {'project_slug': 'helloworld'}
 | |
|             - {'author': 'bob', 'project_slug': 'foobar'}
 | |
|         """
 | |
|         return request.param
 | |
| 
 | |
| The same helper can be used in combination with ``pytest.mark.parametrize``.
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
| 
 | |
|     @pytest.mark.parametrize(
 | |
|         'context, expected_response_code',
 | |
|         [
 | |
|             (pytest.fixture_request('default_context'), 0),
 | |
|             (pytest.fixture_request('extra_context'), 0),
 | |
|         ],
 | |
|     )
 | |
|     def test_generate_project(cookies, context, exit_code):
 | |
|         """Call the cookiecutter API to generate a new project from a
 | |
|         template.
 | |
|         """
 | |
|         result = cookies.bake(extra_context=context)
 | |
| 
 | |
|         assert result.exit_code == exit_code
 |