Hacer que las pruebas formen parte de la aplicación

Hoy voy a discutir una idea completamente nueva para muchos usuarios (especialmente para los pitonistas): integrar pruebas en su aplicación.





Entonces empecemos.





Estado actual

Hoy en día, el problema de la interconexión del código fuente y las pruebas es tal que envía el código fuente a los usuarios de su biblioteca y, por lo general, no incluye sus pruebas en él.





  , , . .





, , .





: Django View, .





from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse

@login_required
def my_view(request: HttpRequest) -> HttpRespose:
    ...
      
      



, :





  1. -









, , ?





API :





# tests/test_views/test_my_view.py
from myapp.views import my_view

def test_authed_successfully(user):
    """Test case for our own logic."""

# Not authed case:
my_view.test_not_authed()
      
      



– – , !





. , Django . :





from django.views.decorators.cache import never_cache
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods

@require_http_methods(['GET', 'POST'])
@login_required
@never_cache
def my_view(request: HttpRequest) -> HttpRespose:
    ...
      
      



, API :





# tests/test_views/test_my_view.py
from myapp.views import my_view

my_view.run_tests()
      
      



:





  1. HTTP





  2. HTTP





  3. Cache-Control















, , — « » , , HTTP- .





, API . , , Django.





( ), . !





deal

deal .





, , ( , Python).





, ( Python int



):





import deal

@deal.pre(lambda a, b: a >= 0 and b >= 0)
@deal.raises(ZeroDivisionError)  # this function can raise if `b=0`, it is ok
def div(a: int, b: int) -> float:
    return a / b
      
      



:





  • @deal.pre(lambda a, b: a >= 0 and b >= 0)



    ,





  • @deal.raises(ZeroDivisionError)



    ZeroDivisionError



    , -





. , (a: int, b: int) -> float



, : mypy



.





(, !): 





div(1, 2)  # ok
div(1, 0)  # ok, runtime ZeroDivisionError

div(-1, 1)  # not ok
# deal.PreContractError: expected a >= 0 and b >= 0 (where a=-1, b=1)
      
      



, . :





import deal

@deal.pre(lambda a, b: a >= 0 and b >= 0)
@deal.raises(ZeroDivisionError)  # this function can raise if `b=0`, it is ok
def div(a: int, b: int) -> float:
    if a > 50:  # Custom, in real life this would be a bug in our logic:
        raise Exception('Oh no! Bug happened!')
    return a / b
      
      



, deal



. , , — :





import deal

from my_lib import div

@deal.cases(div)  # That's all we have to do to test deal-based functions!
def test_div(case: deal.TestCase) -> None:
    case()
      
      



:





» pytest test_deal.py
============================= test session starts ==============================
collected 1 item

test_deal.py F                                                            [100%]

=================================== FAILURES ===================================
___________________________________ test_div ___________________________________

a = 51, b = 0

    @deal.raises(ZeroDivisionError)
    @deal.pre(lambda a, b: a >= 0 and b >= 0)
    def div(a: int, b: int) -> float:
        if a > 50:
>           raise Exception('Oh no! Bug happened!')
E           Exception: Oh no! Bug happened!

test_deal.py:8: Exception
============================== 1 failed in 0.35s ===============================
      
      



, ! ?





:





  • ? hypothesis. , .





. int, def div(a: int, b: int)



. ,  >= 0



, @deal.pre(lambda a, b: a >= 0 and b >= 0)



.





, . . .





  • ZeroDivisionError



    , Exception



    ? : . - — . ZeroDivisionError



    deal.raises



    . , , ( ). , Exception , .





  • ? . — . , . , , . , .





, . , .





, . , deal — deal-solver, . , .





dry-python/returns

dry-python/returns — , Python.





, /. , - .





, . , , , .





« ».





Equable. . Python ==



. .equals()



, .





:





from returns.io import IO

IO(1) == 1  # type-checks, but pointless, always false

IO(1).equals(1)  # does not type-check at all
# error: Argument 1 has incompatible type "int";
# expected "KindN[IO[Any], Any, Any, Any]"

other: IO[int]
IO(1).equals(other)  # ok, might be true or false
      
      



:





_EqualType = TypeVar('_EqualType', bound='Equable')

class Equable(object):
    @abstractmethod
    def equals(self: _EqualType, other: _EqualType) -> bool:
        """Type-safe equality check for values of the same type."""
      
      



, ( ):





from returns.interfaces.equable import Equable

class Example(Equable):
    def __init__(self, inner_value: int) -> None:
        self._inner_value = inner_value

    def equals(self, other: 'Example') -> bool:
        return False  # it breaks how `.equals` is supposed to be used!
      
      



, False



inner_value



. - : . , . .





, , :





  • :





  • a.equals(b) == b.equals(a)







  • : a



    b



    , b



    c



    , a



    c







, , . . .





.





, :





from abc import abstractmethod
from typing import ClassVar, Sequence, TypeVar

from typing_extensions import final

from returns.primitives.laws import (
    Law,
    Law1,
    Law2,
    Law3,
    Lawful,
    LawSpecDef,
    law_definition,
)

_EqualType = TypeVar('_EqualType', bound='Equable')


@final
class _LawSpec(LawSpecDef):  # LOOKATME: our laws def!
    @law_definition
    def reflexive_law(
        first: _EqualType,
    ) -> None:
        """Value should be equal to itself."""
        assert first.equals(first)

    @law_definition
    def symmetry_law(
        first: _EqualType,
        second: _EqualType,
    ) -> None:
        """If ``A == B`` then ``B == A``."""
        assert first.equals(second) == second.equals(first)

    @law_definition
    def transitivity_law(
        first: _EqualType,
        second: _EqualType,
        third: _EqualType,
    ) -> None:
        """If ``A == B`` and ``B == C`` then ``A == C``."""
        if first.equals(second) and second.equals(third):
            assert first.equals(third)


class Equable(Lawful['Equable']):
    _laws: ClassVar[Sequence[Law]] = (
        Law1(_LawSpec.reflexive_law),
        Law2(_LawSpec.symmetry_law),
        Law3(_LawSpec.transitivity_law),
    )

    @abstractmethod
    def equals(self: _EqualType, other: _EqualType) -> bool:
        """Type-safe equality check for values of the same type."""
      
      



, « »!





, , . . , hypothesis



, .





, :





  1. , _laws





  2. hypothesis









  3. , ,





, .





API, ! :





# test_example.py
from returns.contrib.hypothesis.laws import check_all_laws
from your_app import Example

check_all_laws(Example, use_init=True)
      
      



:





» pytest test_example.py
============================ test session starts ===============================
collected 3 items

test_example.py .F.                                                   [100%]

=================================== FAILURES ===================================
____________________ test_Example_equable_reflexive_law _____________________
first = 

    @law_definition
    def reflexive_law(
        first: _EqualType,
    ) -> None:
        """Value should be equal to itself."""
>       assert first.equals(first)
E       AssertionError

returns/interfaces/equable.py:32: AssertionError
========================= 1 failed, 2 passed in 0.22s ==========================
      
      



, test_Example_equable_reflexive_law



, equals



Example



False



, reflexive_law



, , (a == a) is True



.





Example



inner_value







class Example(Equable):
    def __init__(self, inner_value: int) -> None:
        self._inner_value = inner_value

    def equals(self, other: 'Example') -> bool:
        return self._inner_value == other._inner_value  # now we are talking!
      
      



:





» pytest test_example.py
============================= test session starts ==============================
collected 3 items

test_example.py ...                                                   [100%]

============================== 3 passed in 1.57s ===============================
      
      



Example



. ! .





hypothesis



, ( returns.contrib.hypothesis.laws



.





, Equable



— , dry-python/returns



, ; , .





, , Monad , .





. , , .





API .





Dicho esto, ¡los casos de uso son realmente muy variados! Como he demostrado, pueden abarcar desde plataformas de aplicaciones web hasta herramientas de arquitectura y bibliotecas (casi) matemáticas.





¡Me gustaría ver más de estas herramientas en el futuro! Con suerte, pude hablar sobre los posibles beneficios para los autores de bibliotecas actuales y futuros.








All Articles