| theme | author | date | paging |
|---|---|---|---|
./theme.json |
Ismael Mendonça |
Oct 2, 2021 |
Slide %d / %d |
Python testing best practices
Agenda
- Testing principles
- Frameworks
- Pruebas funcionales vs Pruebas basadas en clases
- setUp & tearDown
- Patch & Mock
- Raising exceptions
- Datetime
- Recomendaciones para tu CI
Testing principles
Test isolation
Las pruebas no deben producir "side-effects" sobre otras pruebas.
Testing principles
Test isolation
Las pruebas no deben producir "side-effects" sobre otras pruebas.
Posibles causas...
Testing principles
Test isolation
Patch que permanece activo entre varias pruebas
Testing principles
Test isolation
Patch que permanece activo entre varias pruebas
def test_get_gist_urls():
...
### Start the mock without stopping it
mock.patch.object(GithubClient, "_get", return_value=m_response).start()
###
urls = GithubClient.get_gists_urls()
assert urls == [response_urls[0]["url"]]
def test_get_gist_names():
# OMG THIS IS STILL A MOCK!
#
# The mock is still applied as a side-effect from the previous test
assert isinstance(GithubClient._get, mock.Mock)Testing principles
Test isolation
Patch que permanece activo entre varias pruebas
Ejemplo 1
poetry run pytest isolation/1-patch.py
Testing principles
Test isolation
Patch que permanece activo entre varias pruebas
Posibles soluciones...
Testing principles
Test isolation
Patch que permanece activo entre varias pruebas
Context manager
def test_get_gist_urls():
...
# Wrap the code where we want the patch to take effect with a context manager
with mock.patch.object(GithubClient, "_get", return_value=m_response):
urls = GithubClient.get_gists_urls()
assert urls == [response_urls[0]["url"]]
def test_get_gist_names():
# Should fail
assert isinstance(GithubClient._get, mock.Mock)Testing principles
Test isolation
Patch que permanece activo entre varias pruebas
Ejemplo 2
poetry run pytest isolation/2-patch-ok.py
Testing principles
Test isolation
Patch que permanece activo entre varias pruebas
Decorator
# Or you could use a decorator
@mock.patch("pycones21.github_client.requests.get")
def test_get_gist_urls(m_request):
...
m_request.return_value = m_response
urls = GithubClient.get_gists_urls()
assert urls == [response_urls[0]["url"]]
def test_get_gist_names():
# Should also fail
assert isinstance(GithubClient._get, mock.Mock)Testing principles
Test isolation
Patch que permanece activo entre varias pruebas
Ejemplo 3
poetry run pytest isolation/3-patch-ok.py
Testing principles
Test isolation
Datos que persisten entre pruebas
Testing principles
Test isolation
Datos que persisten entre pruebas
def test_create_user():
user = UserStore.create_user(username="ismaelmt", email="ismael@ismael.com")
assert user.username == "ismaelmt"
assert user.email == "ismael@ismael.com"
assert UserStore.count() == 1
def test_user_amount():
# We want that each test have a clean DB
# This test will fail, but it shouldn't.
assert UserStore.count() == 0Testing principles
Test isolation
Datos que persisten entre pruebas
Ejemplo 4
poetry run pytest isolation/4-db.py
Testing principles
Test isolation
Datos que persisten entre pruebas
@pytest.fixture(autouse=True)
def db_fixture():
yield
UserStore.clean()
def test_create_user():
user = UserStore.create_user(username="ismaelmt", email="ismael@ismael.com")
assert user.username == "ismaelmt"
assert user.email == "ismael@ismael.com"
assert UserStore.count() == 1
def test_user_amount():
# We want that each test have a clean DB
# This test will fail, but it shouldn't.
assert UserStore.count() == 0Testing principles
Test isolation
Datos que persisten entre pruebas
Ejemplo 5
poetry run pytest isolation/5-db-ok.py
Testing principles
Test isolation
Compartir estado entre pruebas dentro de una clase
Testing principles
Test isolation
Compartir estado entre pruebas dentro de una clase
class TestLeakUsers:
@classmethod
def setup_class(cls):
cls.user = {"name": "Lola Mento", "username": "iamauser@user.com"}
def test_username(self):
self.user["name"] = "Elsa Murito"
assert self.user["username"] == "iamauser@user.com"
def test_name(self):
# Capasao!??
assert self.user["name"] == "Lola Mento"Importante: Si necesitas compartir estado, hazlo solo para leerlo durante las pruebas, no lo modifiques!
Testing principles
Test isolation
Compartir estado entre pruebas dentro de una clase
Ejemplo 6
poetry run pytest isolation/6-share-state.py
Testing principles
The more you have to mock out to test your code, the worse your code is. The more code you have to instantiate and put in place to be able to test a specific piece of behavior, the worse your code is. The goal is small testable units, along with higher-level integration and functional tests to test that the units cooperate correctly.
30 best practices for software development and testing -- Michael Foord
Testing principles
Pruebas unitarias
Testing principles
Pruebas unitarias
Las pruebas unitarias deben:
Testing principles
Pruebas unitarias
Las pruebas unitarias deben:
- Tratarse como cajas negras: Evita modificar métodos internos o estados.
Testing principles
Pruebas unitarias
Las pruebas unitarias deben:
-
Tratarse como cajas negras: Evita modificar métodos internos o estados.
-
Pequeñas y bien enfocadas: Pruebas muy grandes usualmente es un indicador de código mal estructurado.
Testing principles
Pruebas unitarias
Las pruebas unitarias deben:
-
Tratarse como cajas negras: Evita modificar métodos internos o estados.
-
Pequeñas y bien enfocadas: Pruebas muy grandes usualmente es un indicador de código mal estructurado.
-
Rápidas: "Una prueba que dura más de 0.1 segundos ya no es considerada unitaria"
Testing principles
Recomendaciones generales
Testing principles
Recomendaciones generales
- Tu código debe SIEMPRE incluir pruebas: Una base de código sin pruebas, debe asumirse como defectuosa.
Testing principles
Recomendaciones generales
- Tu código debe SIEMPRE incluir pruebas: Una base de código sin pruebas, debe asumirse como defectuosa.
- El código de pruebas debe tratarse como código de producto: YAGNI, KISS, DRY, patrones de diseño.
Frameworks
Frameworks
Principales frameworks:
- pytest
- unittest
- Nose
- Nose2 ...
Frameworks
Principales frameworks:
- pytest
- unittest
NoseNose2...
Pruebas funcionales vs Pruebas basadas en clases
Pruebas funcionales vs Pruebas basadas en clases
pytest: Permite escribir tests como funciones o como clases.unittest.TestCase: Todas las pruebas son escritas en clases.
Pruebas funcionales vs Pruebas basadas en clases
Funciones en pytest
Pruebas funcionales vs Pruebas basadas en clases
Funciones en pytest
def test_get_fake_store():
user = UserStore.create_user(username="ismaelmt", email="ismael@ismael.com")
assert FakeStore.get("users", "1") == userPruebas funcionales vs Pruebas basadas en clases
Clases en pytest
Pruebas funcionales vs Pruebas basadas en clases
Clases en pytest
class TestFakeStorePytest:
@pytest.fixture(autouse=True)
def fake_store(self):
FakeStore.clean()
def test_get_fake_store_in_class(self):
user = UserStore.create_user(username="ismaelmt", email="ismael@ismael.com")
assert FakeStore.get("users", "1") == userPruebas funcionales vs Pruebas basadas en clases
Clases en unittest
Pruebas funcionales vs Pruebas basadas en clases
Clases en unittest
class TestFakeStore(TestCase):
def setUp(self):
FakeStore.clean()
def test_get_fake_store_in_class(self):
user = UserStore.create_user(username="ismaelmt", email="ismael@ismael.com")
assert FakeStore.get("users", "1") == userPruebas funcionales vs Pruebas basadas en clases
Ejemplo 7
poetry run pytest ./class_test/7-class-fun.pyPruebas funcionales vs Pruebas basadas en clases
Las siguientes son recomendaciones generales:
Pruebas funcionales vs Pruebas basadas en clases
Las siguientes son recomendaciones generales:
- Utiliza clases como una forma de agrupación semántica de tus pruebas.
Pruebas funcionales vs Pruebas basadas en clases
Las siguientes son recomendaciones generales:
- Utiliza clases como una forma de agrupación semántica de tus pruebas.
- Si necesitas compartir estado entre pruebas (setup, class attribute), utiliza clases.
Pruebas funcionales vs Pruebas basadas en clases
Las siguientes son recomendaciones generales:
- Utiliza clases como una forma de agrupación semántica de tus pruebas.
- Si necesitas compartir estado entre pruebas (setup, class attribute), utiliza clases.
- Si necesitas ejecutar un conjunto de pruebas en un mismo core, utiliza clases.
Pruebas funcionales vs Pruebas basadas en clases
Las siguientes son recomendaciones generales:
- Utiliza clases como una forma de agrupación semántica de tus pruebas.
- Si necesitas compartir estado entre pruebas (setup, class attribute), utiliza clases.
- Si necesitas ejecutar un conjunto de pruebas en un mismo core, utiliza clases.
- Usa
fixturespara código que necesites reusar entre funciones o métodos.
setup & teardown
setup & teardown
Si necesitas datos para tus tests, utiliza los metodos setup y teardown para preparar y limpiar los datos respectivamente.
setup & teardown
pytest
setup & teardown
pytest
Recomendacion: fixtures
setup & teardown
pytest
Recomendacion: fixtures
- En pytest nos valemos de
fixtures. Puedes crear una fixture que realice tanto el setup como el teardown. - Diferentes scopes:
package,session,class,module,function.
@pytest.fixture
def db(autouse=True, scope="function"):
# setup
UserStore.create_user(username="test_user", email="test@example.com")
yield
#teardown
FakeStore.clean()setup & teardown
pytest
No recomendado: xunit-style
setup & teardown
pytest
No recomendado: xunit-style
- O puedes usar también setup y teardown al estilo xunit:
setup_class,setup_module,setup_functionteardown_class,teardown_module,teardown_function
setup & teardown
unittest
setup & teardown
unittest
class TestUser(unittest.TestCase):
def setUp(self):
...
@classmethod
def setUpClass(cls):
cls.user = ...setup & teardown
unittest
class TestUser(unittest.TestCase):
def setUp(self):
...
@classmethod
def setUpClass(cls):
cls.user = ...¡Cuidado!
Cuando utilizas setUpClass lo que defines se mantendrá instanciado a nivel de clase,
si modificas alguna de las variables del setUpClass puedes causar algún "side-effect".
Patch & Mock
Mocking
Patch & Mock
Mocking
Librerías:
- unittest.mock
- pytest-mock: Provee un fixture llamado
mocker
Patch & Mock
Mocking
Librerias:
- unittest.mock ✓
pytest-mock: Provee un fixture llamadomocker
Patch & Mock
Mocking
Librerias:
- unittest.mock ✓
pytest-mock: Provee un fixture llamadomockermocker: Automáticamente deshace el patch al final del test.
Patch & Mock
Mocking
Librerias:
- unittest.mock ✓
pytest-mock: Provee un fixture llamadomockermocker: Automáticamente deshace el patch al final del test.- Conveniente, pero puede resultar contraproducente en equipos grandes de ingeniería.
Patch & Mock
Mocking
Librerias:
- unittest.mock ✓
pytest-mock: Provee un fixture llamadomockermocker: Automáticamente deshace el patch al final del test.- Conveniente, pero puede resultar contraproducente en equipos grandes de ingeniería.
- Confusión entre
unittest.mockymocker. No funcionan bien en conjunto.
Patch & Mock
Mocking
Problemas con mocker
def test_mocker_context(mocker):
with mocker.patch.object(UserStore, "count"):
assert isinstance(UserStore.count, mocker.MagicMock)Patch & Mock
Mocking
Ejemplo 8
poetry run pytest mock_patch/8-mocker.py
Patch & Mock
Mocking
Problemas con mocker
def new_count():
return 10
def test_mocker_context(mocker):
with mocker.patch.object(UserStore, "count", new=new_count):
assert isinstance(UserStore.count, mocker.MagicMock)Patch & Mock
Mocking
Ejemplo 9
poetry run pytest mock_patch/9-mocker.py
Patch & Mock
Mocking
class GithubClient:
...
@classmethod
def get_gists(cls):
LIST_GISTS_ENDPOINT = f"{cls.API_HOST}/gists"
response = cls._get(LIST_GISTS_ENDPOINT)
return response
...Patch & Mock
Mocking
call methods en mocks
Patch & Mock
Mocking
call methods en mocks
from unittest import mock
from pycones21.github_client import GithubClient
@mock.patch.object(GithubClient, "_get")
def test_assertion(m_get):
with mock.patch.object(GithubClient, "get_gists_urls") as m_get_gists_urls:
GithubClient.get_gists()
assert m_get_gists_urls.called_once()Patch & Mock
Mocking
Ejemplo 10
poetry run pytest mock_patch/10-mock-call.py
Patch & Mock
Mocking
call methods en mocks
from unittest import mock
from pycones21.github_client import GithubClient
@mock.patch.object(GithubClient, "_get")
def test_assertion(m_get):
with mock.patch.object(GithubClient, "get_gists_urls") as m_get_gists_urls:
GithubClient.get_gists_urls()
m_get_gists_urls.assert_called_once()Patch & Mock
Mocking
Ejemplo 11
poetry run pytest mock_patch/11-mock-call-ok.py
Patch & Mock
Patch
Patch target
Patch & Mock
Patch
Patch target
El objeto al que queremos aplicarle el patch debe cumplir con las siguientes reglas:
Patch & Mock
Patch
Patch target
El objeto al que queremos aplicarle el patch debe cumplir con las siguientes reglas:
- Debe poder ser importado de tu archivo de pruebas.
Patch & Mock
Patch
Patch target
El objeto al que queremos aplicarle el patch debe cumplir con las siguientes reglas:
-
Debe poder ser importado de tu archivo de pruebas.
-
El path debe ser del objeto que va a ser usado y no donde el objeto se define.
Patch & Mock
Patch
Patch target
# service.py
from pycones21.github_client import GithubClient
def call_github():
gists = GithubClient.get_gists()
return gistsPatch & Mock
Patch
Patch target
El patch debe hacerse en service y no en github_client
Patch & Mock
Patch
Patch target
El patch debe hacerse en service y no en github_client
from pycones21.service import call_github
@mock.patch("pycones21.service.GithubClient.get_gists")
def test_get_gists(m_gists):
call_github()
m_gists.assert_called_once()Patch & Mock
Patch
Patch target
Ejemplo 12
poetry run pytest mock_patch/12-patch-target.pyPatch & Mock
Patch
Patch hell
Patch & Mock
Patch
Patch hell
def test_nested_mocks():
with mock.patch.object(FakeStore, "namespace_exists") as m1:
with mock.patch.object(FakeStore, "get") as m2:
with mock.patch.object(FakeStore, "set_namespace") as m3:
with mock.patch.object(FakeStore, "count") as m4:
assert isinstance(m1, mock.MagicMock)
assert isinstance(m2, mock.MagicMock)
assert isinstance(m3, mock.MagicMock)
assert isinstance(m4, mock.MagicMock)Patch & Mock
Patch
Patch hell
La alternativa patch.multiple:
Patch & Mock
Patch
Patch hell
La alternativa patch.multiple:
@mock.patch.multiple(
FakeStore,
namespace_exists=mock.DEFAULT,
get=mock.DEFAULT,
set_namespace=mock.DEFAULT,
count=mock.DEFAULT,
)
def test_multimock(**mocks):
namespace_exists, get, set_namespace, count = mocks.values()
assert isinstance(namespace_exists, mock.MagicMock)
assert isinstance(get, mock.MagicMock)
assert isinstance(set_namespace, mock.MagicMock)
assert isinstance(count, mock.MagicMock)Patch & Mock
Patch
Patch hell
Ejemplo 14
poetry run pytest mock_patch/14-patch-hell.pyRaising exceptions
Raising exceptions
import pytest
def raise_exception():
raise Exception("error")
def test_raises_exception():
with pytest.raises(Exception) as error:
raise_exception()
assert str(error.value) == "Some other string"Ejemplo 15
poetry run pytest exceptions/15-raise-exception.pyRaising exceptions
import pytest
def raise_exception():
raise Exception("error")
def test_raises_exception():
with pytest.raises(Exception) as error:
raise_exception()
# Assert outside of the raises context manager
assert str(error.value) == "Some other string"Raising exceptions
Ejemplo 16
poetry run pytest exceptions/16-raise-exception-ok.pyDatetime
Datetime
import datetime
from freezegun import freeze_time
from datetime import timedelta
def day_tomorrow(today):
return today + timedelta(days=1)
def test_date_naive():
today = datetime.datetime.today()
day_tomorrow = day_tomorrow(today)
assert tomorrow.day == 2Datetime
Ejemplo 16
poetry run pytest time_examples/16-dates.pyDatetime
Alternativas
Datetime
Alternativas
import datetime
from freezegun import freeze_time
def test_day_injection():
today = datetime.datetime(2021, 10, 2)
tomorrow = day_tomorrow(today)
assert tomorrow == 3Datetime
Alternativas
import datetime
from freezegun import freeze_time
@freeze_time("2021-10-02")
def test_date_naive():
today = datetime.datetime.today()
tomorrow = day_tomorrow(today)
assert tomorrow == 3Datetime
Ejemplo 17
poetry run pytest time_examples/17-dates.pyBuenas prácticas en CI (Continuous Integration)
Buenas prácticas en CI (Continuous Integration)
Test coverage
- 100% test coverage no implica 100% de calidad.
Buenas prácticas en CI (Continuous Integration)
Test coverage
- 100% test coverage no implica 100% de calidad.
- Sin embargo, es buena práctica tener un mínimo nivel que nos permita garantizar la cobertura del código.
Buenas prácticas en CI (Continuous Integration)
Test coverage
- 100% test coverage no implica 100% de calidad.
- Sin embargo, es buena práctica tener un mínimo nivel que nos permita garantizar la cobertura del código.
- Coverage >80% o >90% suele ser lo más recomendado en backend.
Buenas prácticas en CI (Continuous Integration)
Test coverage
- 100% test coverage no implica 100% de calidad.
- Sin embargo, es buena práctica tener un mínimo nivel que nos permita garantizar la cobertura del código.
- Coverage >80% o >90% suele ser lo más recomendado en backend.
- Con librerías como pytest podemos pedir un mínimo de coverage en CI:
pytest --cov-fail-under=90Buenas prácticas en CI (Continuous Integration)
Paralelización de pruebas
Buenas prácticas en CI (Continuous Integration)
Paralelización de pruebas
pytest
Buenas prácticas en CI (Continuous Integration)
Paralelización de pruebas
pytest
Con pytest-xdist puedes ejecutar pruebas paralelas especificando el número de cores:
poetry run pytest -n2 isolation/1-patch.pyBuenas prácticas en CI (Continuous Integration)
Paralelización de pruebas
Django
Buenas prácticas en CI (Continuous Integration)
Paralelización de pruebas
Django
python manage.py test --parallelBuenas prácticas en CI (Continuous Integration)
Test reports
- Incluye coverage reports que indiquen el nivel de cobertura del código y las líneas cubiertas.
poetry run pytest --cov=. isolation/1-patch.pyBuenas prácticas en CI (Continuous Integration)
Test reports
- Incluye coverage reports que indiquen el nivel de cobertura del código y las líneas cubiertas.
poetry run pytest --cov=. isolation/1-patch.py- Reporte de flaky/heisen tests