-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial version, forked from pytest-cases.
- Loading branch information
Showing
10 changed files
with
589 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -102,3 +102,13 @@ venv.bak/ | |
|
||
# mypy | ||
.mypy_cache/ | ||
|
||
# Pycharm | ||
.idea/ | ||
|
||
# Mkdocs | ||
site/ | ||
|
||
# travis CI | ||
github_travis_rsa* | ||
reports |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,74 @@ | ||
# python-pytest-steps | ||
A tiny package to ease the creation of test steps with shared intermediate results/state | ||
# pytest-steps | ||
|
||
Create step-wise / incremental tests in `pytest`. | ||
|
||
[](https://travis-ci.org/smarie/python-pytest-steps) [](https://smarie.github.io/python-pytest-steps/junit/report.html) [](https://codecov.io/gh/smarie/python-pytest-steps) [](https://smarie.github.io/python-pytest-steps/) [](https://pypi.python.org/pypi/pytest_steps/) | ||
|
||
**This is the readme for developers.** The documentation for users is available here: [https://smarie.github.io/python-pytest-steps/](https://smarie.github.io/python-pytest-steps/) | ||
|
||
## Want to contribute ? | ||
|
||
Contributions are welcome ! Simply fork this project on github, commit your contributions, and create pull requests. | ||
|
||
Here is a non-exhaustive list of interesting open topics: [https://github.com/smarie/python-pytest-steps/issues](https://github.com/smarie/python-pytest-steps/issues) | ||
|
||
## Running the tests | ||
|
||
This project uses `pytest`. | ||
|
||
```bash | ||
pytest -v pytest_steps/tests/ | ||
``` | ||
|
||
You may need to install requirements for setup beforehand, using | ||
|
||
```bash | ||
pip install -r ci_tools/requirements-test.txt | ||
``` | ||
|
||
## Packaging | ||
|
||
This project uses `setuptools_scm` to synchronise the version number. Therefore the following command should be used for development snapshots as well as official releases: | ||
|
||
```bash | ||
python setup.py egg_info bdist_wheel rotate -m.whl -k3 | ||
``` | ||
|
||
You may need to install requirements for setup beforehand, using | ||
|
||
```bash | ||
pip install -r ci_tools/requirements-setup.txt | ||
``` | ||
|
||
## Generating the documentation page | ||
|
||
This project uses `mkdocs` to generate its documentation page. Therefore building a local copy of the doc page may be done using: | ||
|
||
```bash | ||
mkdocs build | ||
``` | ||
|
||
You may need to install requirements for doc beforehand, using | ||
|
||
```bash | ||
pip install -r ci_tools/requirements-doc.txt | ||
``` | ||
|
||
## Generating the test reports | ||
|
||
The following commands generate the html test report and the associated badge. | ||
|
||
```bash | ||
pytest --junitxml=junit.xml -v pytest_steps/tests/ | ||
ant -f ci_tools/generate-junit-html.xml | ||
python ci_tools/generate-junit-badge.py | ||
``` | ||
|
||
### PyPI Releasing memo | ||
|
||
This project is now automatically deployed to PyPI when a tag is created. Anyway, for manual deployment we can use: | ||
|
||
```bash | ||
twine upload dist/* -r pypitest | ||
twine upload dist/* | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from pytest_steps.steps import test_steps, ResultsHolder | ||
|
||
__all__ = [ | ||
# the submodule | ||
'steps', | ||
# all symbols imported above | ||
'test_steps', 'ResultsHolder' | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
from functools import lru_cache | ||
from inspect import signature, getmodule | ||
|
||
import pytest | ||
|
||
|
||
class ResultsHolder: | ||
""" | ||
An object that is passed along the various steps of your tests. | ||
You can put intermediate results in here, and find them in the following steps. | ||
Note: you can use `vars(results)` to see the available results. | ||
""" | ||
pass | ||
|
||
|
||
def test_steps(*steps, test_step_argname: str='test_step', test_results_argname: str='results'): | ||
""" | ||
Decorates a test function so as to automatically parametrize it with all steps listed as arguments. | ||
When the steps are functions, this is equivalent to | ||
`@pytest.mark.parametrize(test_step_argname, steps, ids=lambda x: x.__name__)` | ||
```python | ||
from pytest_steps import test_steps | ||
def step_a(): | ||
# perform this step | ||
print("step a") | ||
assert not False | ||
def step_b(): | ||
# perform this step | ||
print("step b") | ||
assert not False | ||
@test_steps(step_a, step_b) | ||
def test_suite_no_results(test_step): | ||
# Execute the step | ||
test_step() | ||
``` | ||
You can add a 'results' parameter to your test function if you wish to share a `ResultsHolder` object between your | ||
steps. | ||
```python | ||
def step_a(results: ResultsHolder): | ||
# perform this step | ||
print("step a") | ||
assert not False | ||
# intermediate results can be stored in results | ||
results.intermediate_a = 'some intermediate result created in step a' | ||
def step_b(results: ResultsHolder): | ||
# perform this step, leveraging the previous step's results | ||
print("step b") | ||
new_text = results.intermediate_a + " ... augmented" | ||
print(new_text) | ||
assert len(new_text) == 56 | ||
@test_steps(step_a, step_b) | ||
def test_suite_with_results(test_step, results: ResultsHolder): | ||
# Execute the step with access to the results holder | ||
test_step(results) | ||
``` | ||
:param steps: a list of test steps. They can be anything, but typically they are non-test (not prefixed with 'test') | ||
functions. | ||
:param test_step_argname: the optional name of the function argument that will receive the test step object. | ||
Default is 'test_step'. | ||
:param test_results_argname: the optional name of the function argument that will receive the shared `ResultsHolder` | ||
object if present. Default is 'results'. | ||
:return: | ||
""" | ||
def steps_decorator(test_func): | ||
""" | ||
The generated test function decorator. | ||
It is equivalent to @mark.parametrize('case_data', cases) where cases is a tuple containing a CaseDataGetter for | ||
all case generator functions | ||
:param test_func: | ||
:return: | ||
""" | ||
def get_id(f): | ||
if callable(f) and hasattr(f, '__name__'): | ||
return f.__name__ | ||
else: | ||
return str(f) | ||
|
||
step_ids = [get_id(f) for f in steps] | ||
|
||
# Finally create the pytest decorator and apply it | ||
# depending on the presence of test_results_argname in signature | ||
s = signature(test_func) | ||
if test_results_argname in s.parameters: | ||
# the user wishes to share results across test steps. Create a cached fixture | ||
@lru_cache(maxsize=None) | ||
def get_results_holder(**kwargs): | ||
""" | ||
A factory for the ResultsHolder objects. Since it uses @lru_cache, the same ResultsHolder will be | ||
returned when the keyword arguments are the same. | ||
:param kwargs: | ||
:return: | ||
""" | ||
return ResultsHolder() # TODO use Munch or MaxiMunch from `mixture` project, when publicly available ? | ||
|
||
@pytest.fixture(name=test_results_argname) | ||
def results(request): | ||
""" | ||
The fixture for the ResultsHolder. It implements an intelligent cache so that the same ResultsHolder | ||
object is used across test steps. | ||
:param request: | ||
:return: | ||
""" | ||
# The object should be different everytime anything changes, except when the test step changes | ||
dont_change_when_these_change = {test_step_argname} | ||
|
||
# We also do not want the 'results' itself nor the pytest 'request' to be taken into account, since | ||
# the first is not yet defined and the second is an internal pytest variable | ||
dont_change_when_these_change.update({test_results_argname, 'request'}) | ||
|
||
# List the values of all the test function parameters that matter | ||
kwargs = {argname: request.getfuncargvalue(argname) | ||
for argname in request.funcargnames | ||
if argname not in dont_change_when_these_change} | ||
|
||
# Get or create the cached Result holder for this combination of parameters | ||
return get_results_holder(**kwargs) | ||
|
||
# Add the fixture dynamically: we have to add it to the function holder module as explained in | ||
# https://github.com/pytest-dev/pytest/issues/2424 | ||
module = getmodule(test_func) | ||
if test_results_argname not in dir(module): | ||
setattr(module, test_results_argname, results) | ||
else: | ||
raise ValueError("The {} fixture already exists in module {}: please specify a different " | ||
"`test_results_argname` in `@test_steps`".format(test_results_argname, module)) | ||
|
||
# Finally parametrize the function with the test steps | ||
parametrizer = pytest.mark.parametrize(test_step_argname, steps, ids=step_ids) | ||
return parametrizer(test_func) | ||
|
||
return steps_decorator | ||
|
||
|
||
test_steps.__test__ = False # to prevent pytest to think that this is a test ! |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
from functools import lru_cache | ||
import pytest | ||
|
||
|
||
# ------------------- The same code than what is generated dynamically by @test_steps | ||
class ResultsHolder: | ||
pass | ||
|
||
|
||
@lru_cache(maxsize=None) | ||
def get_results_holder(**kwargs): | ||
""" | ||
A factory for the ResultsHolder objects. Since it uses @lru_cache, the same ResultsHolder will be returned when | ||
the keyword arguments are the same. | ||
:param kwargs: | ||
:return: | ||
""" | ||
return ResultsHolder() | ||
|
||
|
||
@pytest.fixture | ||
def results(request): | ||
""" | ||
The fixture for the ResultsHolder | ||
:param request: | ||
:return: | ||
""" | ||
kwargs = {argname: request.getfuncargvalue(argname) | ||
for argname in request.funcargnames | ||
if argname not in {'test_step', 'results', 'request'}} | ||
return get_results_holder(**kwargs) | ||
# ------------------------------------- | ||
|
||
|
||
def step_a(results: ResultsHolder, stupid_param): | ||
""" Step a of the test """ | ||
|
||
# perform this step | ||
print("step a - " + stupid_param) | ||
assert not False | ||
|
||
# Assert that the ResultsHolder object is a brand new one everytime we start this step | ||
assert not hasattr(results, 'intermediate_a') | ||
|
||
# intermediate results can be stored in results | ||
results.intermediate_a = 'some intermediate result created in step a for test ' + stupid_param | ||
|
||
assert not hasattr(results, 'p') | ||
results.p = stupid_param | ||
|
||
|
||
def step_b(results: ResultsHolder, stupid_param): | ||
""" Step b of the test """ | ||
|
||
# perform this step | ||
print("step b - " + stupid_param) | ||
|
||
# assert that step a has been done | ||
assert hasattr(results, 'intermediate_a') | ||
# ... and that the resultsholder object that we get is the one for our test suite (same parameters) | ||
assert results.intermediate_a == 'some intermediate result created in step a for test ' + stupid_param | ||
|
||
new_text = results.intermediate_a + " ... augmented" | ||
print(new_text) | ||
|
||
assert results.p == stupid_param | ||
|
||
|
||
@pytest.fixture(params=['F', 'G']) | ||
def fix(request): | ||
return request.param | ||
|
||
|
||
@pytest.mark.parametrize('really_stupid_param2', ["2A", "2B"]) | ||
@pytest.mark.parametrize('test_step', [step_a, step_b]) | ||
@pytest.mark.parametrize('stupid_param1', ["1a", "1b"]) | ||
def test_manual_pytest_equivalent(test_step, stupid_param1, really_stupid_param2, fix, results: ResultsHolder): | ||
"""This test performs the same thing than @test_steps but manually. | ||
See the test_steps_with_results.py for details""" | ||
|
||
# Execute the step | ||
test_step(results, stupid_param1 + really_stupid_param2 + fix) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from pytest_steps import test_steps | ||
|
||
|
||
def step_a(): | ||
""" Step a of the test """ | ||
|
||
# perform this step | ||
print("step a") | ||
assert not False | ||
|
||
|
||
def step_b(): | ||
""" Step b of the test """ | ||
|
||
# perform this step | ||
print("step b") | ||
assert not False | ||
|
||
|
||
# equivalent to | ||
# @pytest.mark.parametrize('test_step', (step_check_a, step_check_b), ids=lambda x: x.__name__) | ||
@test_steps(step_a, step_b) | ||
def test_suite_no_results(test_step): | ||
""" """ | ||
|
||
# Execute the step | ||
test_step() |
Oops, something went wrong.