theme | author | date | paging |
---|---|---|---|
./theme.json |
Ismael Mendonça |
Oct 2, 2021 |
Slide %d / %d |
- Testing principles
- Frameworks
- Pruebas funcionales vs Pruebas basadas en clases
- setUp & tearDown
- Patch & Mock
- Raising exceptions
- Datetime
- Recomendaciones para tu CI
Las pruebas no deben producir "side-effects" sobre otras pruebas.
Las pruebas no deben producir "side-effects" sobre otras pruebas.
Posibles causas...
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)
Ejemplo 1
poetry run pytest isolation/1-patch.py
Posibles soluciones...
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)
Ejemplo 2
poetry run pytest isolation/2-patch-ok.py
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)
Ejemplo 3
poetry run pytest isolation/3-patch-ok.py
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() == 0
Ejemplo 4
poetry run pytest isolation/4-db.py
@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() == 0
Ejemplo 5
poetry run pytest isolation/5-db-ok.py
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!
Ejemplo 6
poetry run pytest isolation/6-share-state.py
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
Las pruebas unitarias deben:
Las pruebas unitarias deben:
- Tratarse como cajas negras: Evita modificar métodos internos o estados.
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.
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"
- Tu código debe SIEMPRE incluir pruebas: Una base de código sin pruebas, debe asumirse como defectuosa.
- 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.
Principales frameworks:
- pytest
- unittest
- Nose
- Nose2 ...
Principales frameworks:
- pytest
- unittest
NoseNose2...
pytest
: Permite escribir tests como funciones o como clases.unittest.TestCase
: Todas las pruebas son escritas en clases.
Funciones en pytest
Funciones en pytest
def test_get_fake_store():
user = UserStore.create_user(username="ismaelmt", email="ismael@ismael.com")
assert FakeStore.get("users", "1") == user
Clases en pytest
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") == user
Clases en unittest
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") == user
Ejemplo 7
poetry run pytest ./class_test/7-class-fun.py
Las siguientes son recomendaciones generales:
Las siguientes son recomendaciones generales:
- Utiliza clases como una forma de agrupación semántica de tus pruebas.
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.
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.
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
fixtures
para código que necesites reusar entre funciones o métodos.
Si necesitas datos para tus tests, utiliza los metodos setup y teardown para preparar y limpiar los datos respectivamente.
Recomendacion: fixtures
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()
No recomendado: xunit-style
No recomendado: xunit-style
- O puedes usar también setup y teardown al estilo xunit:
setup_class
,setup_module
,setup_function
teardown_class
,teardown_module
,teardown_function
class TestUser(unittest.TestCase):
def setUp(self):
...
@classmethod
def setUpClass(cls):
cls.user = ...
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".
Librerías:
- unittest.mock
- pytest-mock: Provee un fixture llamado
mocker
Librerias:
- unittest.mock ✓
pytest-mock: Provee un fixture llamadomocker
Librerias:
- unittest.mock ✓
pytest-mock: Provee un fixture llamadomocker
mocker
: Automáticamente deshace el patch al final del test.
Librerias:
- unittest.mock ✓
pytest-mock: Provee un fixture llamadomocker
mocker
: Automáticamente deshace el patch al final del test.- Conveniente, pero puede resultar contraproducente en equipos grandes de ingeniería.
Librerias:
- unittest.mock ✓
pytest-mock: Provee un fixture llamadomocker
mocker
: Automáticamente deshace el patch al final del test.- Conveniente, pero puede resultar contraproducente en equipos grandes de ingeniería.
- Confusión entre
unittest.mock
ymocker
. No funcionan bien en conjunto.
Problemas con mocker
def test_mocker_context(mocker):
with mocker.patch.object(UserStore, "count"):
assert isinstance(UserStore.count, mocker.MagicMock)
Ejemplo 8
poetry run pytest mock_patch/8-mocker.py
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)
Ejemplo 9
poetry run pytest mock_patch/9-mocker.py
class GithubClient:
...
@classmethod
def get_gists(cls):
LIST_GISTS_ENDPOINT = f"{cls.API_HOST}/gists"
response = cls._get(LIST_GISTS_ENDPOINT)
return response
...
call
methods en mocks
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()
Ejemplo 10
poetry run pytest mock_patch/10-mock-call.py
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()
Ejemplo 11
poetry run pytest mock_patch/11-mock-call-ok.py
El objeto al que queremos aplicarle el patch debe cumplir con las siguientes reglas:
El objeto al que queremos aplicarle el patch debe cumplir con las siguientes reglas:
- Debe poder ser importado de tu archivo de pruebas.
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.
# service.py
from pycones21.github_client import GithubClient
def call_github():
gists = GithubClient.get_gists()
return gists
El patch debe hacerse en service
y no en github_client
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()
Ejemplo 12
poetry run pytest mock_patch/12-patch-target.py
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)
La alternativa patch.multiple
:
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)
Ejemplo 14
poetry run pytest mock_patch/14-patch-hell.py
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.py
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"
Ejemplo 16
poetry run pytest exceptions/16-raise-exception-ok.py
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 == 2
Ejemplo 16
poetry run pytest time_examples/16-dates.py
Alternativas
Alternativas
import datetime
from freezegun import freeze_time
def test_day_injection():
today = datetime.datetime(2021, 10, 2)
tomorrow = day_tomorrow(today)
assert tomorrow == 3
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 == 3
Ejemplo 17
poetry run pytest time_examples/17-dates.py
- 100% test coverage no implica 100% de calidad.
- 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.
- 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.
- 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=90
Con pytest-xdist
puedes ejecutar pruebas paralelas especificando el número de cores:
poetry run pytest -n2 isolation/1-patch.py
python manage.py test --parallel
- 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
- 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