{ "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 }