Files
learn-python3/notebooks/beginner/testing2.ipynb
2018-05-25 09:21:00 +02:00

202 lines
6.2 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Testing with [pytest](https://docs.pytest.org/en/latest/) - part 2"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Let's make sure pytest and ipytest packages are installed\n",
"# ipytest is required for running pytest inside Jupyter notebooks\n",
"import sys\n",
"!{sys.executable} -m pip install pytest\n",
"!{sys.executable} -m pip install ipytest\n",
"\n",
"import ipytest.magics\n",
"import pytest\n",
"__file__ = 'testing2.ipynb'"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## [`@pytest.fixture`](https://docs.pytest.org/en/latest/fixture.html#pytest-fixtures-explicit-modular-scalable)\n",
"Let's consider we have an implemention of `Person` class which we want to test."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# This would be e.g. in person.py\n",
"class Person:\n",
" def __init__(self, first_name, last_name, age):\n",
" self.first_name = first_name\n",
" self.last_name = last_name\n",
" self.age = age\n",
" \n",
" @property\n",
" def full_name(self):\n",
" return '{} {}'.format(self.first_name, self.last_name)\n",
" \n",
" @property\n",
" def as_dict(self):\n",
" return {'name': self.full_name, 'age': self.age}\n",
" \n",
" def increase_age(self, years):\n",
" if years < 0:\n",
" raise ValueError('Can not make people younger :(')\n",
" self.age += years"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You can easily create resusable testing code by using pytest fixtures. If you introduce your fixtures inside [_conftest.py_](https://docs.pytest.org/en/latest/fixture.html#conftest-py-sharing-fixture-functions), the fixtures are available for all your test cases. In general, the location of _conftest.py_ is at the root of your _tests_ directory."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# This would be in either conftest.py or test_person.py\n",
"@pytest.fixture()\n",
"def default_person():\n",
" person = Person(first_name='John', last_name='Doe', age=82)\n",
" return person"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Then you can utilize `default_person` fixture in the actual test cases. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%run_pytest[clean]\n",
"\n",
"# These would be in test_person.py\n",
"def test_full_name(default_person): # Note: we use fixture as an argument of the test case\n",
" result = default_person.full_name\n",
" assert result == 'John Doe'\n",
" \n",
" \n",
"def test_as_dict(default_person):\n",
" expected = {'name': 'John Doe', 'age': 82}\n",
" result = default_person.as_dict\n",
" assert result == expected\n",
" \n",
" \n",
"def test_increase_age(default_person):\n",
" default_person.increase_age(1)\n",
" assert default_person.age == 83\n",
" \n",
" default_person.increase_age(10)\n",
" assert default_person.age == 93\n",
" \n",
" \n",
"def test_increase_age_with_negative_number(default_person):\n",
" with pytest.raises(ValueError):\n",
" default_person.increase_age(-1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"By using a fixture, we could use the same `default_person` for all our four test cases!\n",
"\n",
"In the `test_increase_age_with_negative_number` we used [`pytest.raises`](https://docs.pytest.org/en/latest/assert.html#assertions-about-expected-exceptions) to verify that an exception is raised. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## [`@pytest.mark.parametrize`](https://docs.pytest.org/en/latest/parametrize.html#pytest-mark-parametrize-parametrizing-test-functions)\n",
"Sometimes you want to test the same functionality with multiple different inputs. `pytest.mark.parametrize` is your solution for defining multiple different inputs with expected outputs. Let's consider the following implementation of `replace_names` function. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# This would be e.g. in string_manipulate.py\n",
"def replace_names(original_str, new_name):\n",
" \"\"\"Replaces names (uppercase words) of original_str by new_name\"\"\"\n",
" words = original_str.split()\n",
" manipulated_words = [new_name if w.istitle() else w for w in words]\n",
" return ' '.join(manipulated_words)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can test the `replace_names` function with multiple inputs by using `pytest.mark.parametrize`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%run_pytest[clean]\n",
"\n",
"# This would be in your test module\n",
"@pytest.mark.parametrize(\"original,new_name,expected\", [\n",
" ('this is Lisa', 'John Doe', 'this is John Doe'),\n",
" ('how about Frank and Amy', 'John', 'how about John and John'),\n",
" ('no names here', 'John Doe', 'no names here'),\n",
" ])\n",
"def test_replace_names(original, new_name, expected):\n",
" result = replace_names(original, new_name)\n",
" assert result == expected"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.5.4"
}
},
"nbformat": 4,
"nbformat_minor": 2
}