今日は、多くのユーザー(特にpythonists)にとって、テストをアプリケーションに統合するというまったく新しいアイデアについて説明します。
それでは始めましょう。
現在の状態
今日、ソースコードとテストの相互接続の問題は、ライブラリのユーザーにソースコードを送信し、ほとんどの場合、テストをライブラリにまったく含めないことです。
, , . .
, , .
: Django View, .
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse
@login_required
def my_view(request: HttpRequest) -> HttpRespose:
...
, :
-
, , ?
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()
– – , !
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()
:
HTTP
HTTP
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
, .
, :
, _laws
hypothesis
, ,
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 .
そうは言っても、ユースケースは非常に多様です!私が示したように、それらはWebアプリケーションプラットフォームからアーキテクチャツールや(ほぼ)数学ライブラリにまで及ぶ可能性があります。
将来的にはこれらのツールをもっと見たいです!うまくいけば、現在および将来のライブラリ作成者にとって考えられるメリットについて話すことができました。