Tests zu einem Teil der Anwendung machen

Heute werde ich eine völlig neue Idee für viele Benutzer (insbesondere für Pythonisten) diskutieren: die Integration von Tests in Ihre Anwendung.





Also lasst uns anfangen.





Aktueller Status

Heutzutage besteht das Problem der Verbindung von Quellcode und Tests darin, dass Sie den Quellcode an die Benutzer Ihrer Bibliothek senden und Ihre Tests meistens ĂĽberhaupt nicht darin enthalten.





  , , . .





, , .





: 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 .





Davon abgesehen sind die Anwendungsfälle wirklich sehr unterschiedlich! Wie ich gezeigt habe, können sie von Webanwendungsplattformen über Architekturwerkzeuge bis hin zu (nahezu) mathematischen Bibliotheken reichen.





Ich würde gerne in Zukunft mehr von diesen Tools sehen! Hoffentlich konnte ich über die möglichen Vorteile für aktuelle und zukünftige Bibliotheksautoren sprechen.








All Articles