165 lines
		
	
	
		
			4.2 KiB
		
	
	
	
		
			ReStructuredText
		
	
	
	
			
		
		
	
	
			165 lines
		
	
	
		
			4.2 KiB
		
	
	
	
		
			ReStructuredText
		
	
	
	
| :orphan:
 | |
| 
 | |
| ===================================
 | |
| PROPOSAL: Parametrize with fixtures
 | |
| ===================================
 | |
| 
 | |
| .. warning::
 | |
| 
 | |
|     This document outlines a proposal around using fixtures as input
 | |
|     of parametrized tests or 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`` would tell pytest to yield
 | |
| all parameters marked as a fixture.
 | |
| 
 | |
| .. note::
 | |
| 
 | |
|     The `pytest-lazy-fixture <https://pypi.org/project/pytest-lazy-fixture/>`_ plugin implements a very
 | |
|     similar solution to the proposal below, make sure to check it out.
 | |
| 
 | |
| .. 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
 |