테스트를 애플리케이션의 일부로 만들기
오늘, 나는 파이썬 사용자를 위해 테스트가 응용 프로그램의 가치 있는 일부분이 되도록 하는 새로운 아이디어를 토론할 것이다.
자, 시작합시다.
현상
현재 원본 코드/테스트 이원론의 현황은 원본 코드를 라이브러리 사용자에게 보내고 테스트를 어떤 방식으로도 포함하지 않는다는 것이다.
때때로 사람들은
tests/
폴더를 발행판에 추가하기 때문에 만일을 대비하고 있을 뿐이다.대부분의 경우 최종 사용자에게 무용지물이다.가장 중요한 것은 우리 사용자들이 자신이 이런 상황에 처해 있다는 것을 자주 발견할 수 있다는 것이다. 그들은 반드시 도서관에 특정한 물건을 위해 테스트를 다시 실시해야 한다.
예를 들겠습니다. Django 보기는 권한을 부여받은 사용자만 사용할 수 있습니다.
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()
그리고--펑--우리는 한 줄의 코드로 두 번째 용례를 테스트했다!그리고 그것뿐만이 아니다.예를 들어 Django에서는 함수 decorators을 스택하여 여러 작업을 수행할 수 있습니다.이런 상황을 상상해 보세요.
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()
이 가능하다, ~할 수 있다,...Cache-Control
제목 및 올바른 값 이 장의 잘못된 점은 논의된 API가 존재하지 않는다는 점입니다.그리고 영원히 Django에 존재하지 않을 수도 있습니다.
그러나 다른 잘 알려지지 않은 프로젝트(하지만 내가 유지보수를 돕는 프로젝트)는 이미 이런 기능을 갖추고 있다.녀석들로 뭘 할 수 있는지 보여줘!
처리하다.
deal
은 Design-by-Contract의 도서관입니다.함수와 클래스를 형식적으로 표시할 수 없는 추가 검사를 허용합니다. (적어도 pythonland에서는 그렇습니다.)
함수가 있다고 가정하면 두 개의 정수(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)
이어야 한다는 것이다.우리는 생성된 예시 수량을 제어하고 다른 작은 조정을 진행할 수 있다.
자세한 내용은 docs을 참조하십시오.
ZeroDivisionError
은 테스트를 깨지 않았고, Exception
은 테스트를 깨지 않았습니까?이것이 바로 계약의 작업 원리이기 때문이다. 당신은 모든 가능한 상황을 명확하게 정의했기 때문이다.이상한 일이 생기면 계약이 위반된다.우리의 예시에서
ZeroDivisionError
은 deal.raises
decorator를 통해 체결한 계약의 일부분이다.그래서 우리는 그것이 발생할 수 있다는 것을 안다.이것이 바로 우리가 이를 테스트 실패로 보지 않고 raw Exception
은 우리 계약의 일부분이 아니며 실패로 간주하는 이유입니다.이것은 가장 재미있는 문제다.답은 정해지지 않았다. 슬프지만 정말이었다.
그 중에는 무궁무진한 용례, 논리, 충돌과 결함이 있다.그리고 나는 프로그램의 모든 빈틈을 포착할 수 없다고 확신한다.
우리는 심지어 우리의 계약을 Theorems to be proved으로 표현할 수 있다.
예를 들어
deal
은 현재 진행 중인 연구 파트너 프로젝트인 deal-solver
을 가지고 있어 도움이 될 수 있다.하지만 이것은 다른 문장의 주제입니다. 이제 계속합시다.건python/반환
dry-python/returns
은 원어를 가진 라이브러리로 파이톤의 유형화 함수 프로그래밍을 더욱 쉽게 한다.내부에 우리는 일련의 인터페이스가 있는데 사용자는 자신의 원어/대상을 확장할 수 있다.최근 Higher Kinded Types에 관한 글에서 저는 어떻게 유형별로 안전한 방식으로 이 점을 실현할 수 있는지 보여 드렸습니다.
지금 내가 보여줄 것은 그것만으로는 부족하다는 것이다.가장 가능성이 있는 것은 물체가 어떻게 행동해야 하는지에 대한 추가 규칙이 필요하다는 것이다.
우리는 이 특징을'단자법칙을 가치관으로 삼는다'고 부른다.
신분법
가장 간단한 고급 인터페이스부터 시작하겠습니다.
Equable
.그것은 형식이 안전하게 같은 검사를 허용하는 인터페이스이다.파이톤에서 ==
을 사용할 수 있기 때문이다.그러나 우리의 .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
10b
이 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
의 모든 법률 Source code은 구현 세부 사항에 관심이 있는 사용자에게 적합합니다.
우리는 함수 호출에서 모든 조작을 완성할 수 있도록 최종 사용자에게 간단한 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 = <ex.Example object at 0x104d61b90>
@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
은 우리의 False
류에서 항상 Example
으로 돌아오기 때문이다.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 # no we are talking!
테스트를 다시 실행하려면:» pytest test_example.py
============================= test session starts ==============================
collected 3 items
test_example.py ... [100%]
============================== 3 passed in 1.57s ===============================
그러나 우리는 실제로 Example
을 위한 테스트를 작성하지 않았다.반대로 우리는 미래의 실시를 위해 영원히 법률을 제정했다!이것이 바로 사용자에게 관심을 갖는 모습이다.마찬가지로 awome
hypothesis
은 무작위 데이터를 생성하여 우리의 테스트에 입력함으로써 우리를 돕는다(이것이 바로 이 가방이 왜 returns.contrib.hypothesis.laws
이라고 불리는가).기타 기능의 법칙
물론
Equable
은 우리가 dry-python/returns
에서 가지고 있는 유일한 인터페이스가 아닙니다. 우리는 lots of them을 가지고 있으며 대부분의 전통적인 기능 실례를 포함하고 있습니다. 관심이 있다면 저희 docs을 읽어 주십시오.만약 사람들이
Monad
이 실제로 무엇인지, 그리고 그것이 어떤 규칙이 있는지 알고 싶다면, 이 인터페이스들이 그들을 도울 것이다.그들 중 대다수는 정의에 부착된 법률이 있다.이것은 우리 사용자들이 가능한 한 적은 절차를 통해 그들의 실현이 정확하다는 것을 확보하는 데 도움이 된다.
결론
몇몇 용례에서 테스트를 응용 프로그램과 함께 발표하는 것은 매우 멋진 기능일 수도 있다.
용례가 정말 달라요!내가 보여준 바와 같이 웹 프레임워크에서 구조 도구와 수학 라이브러리에 이르기까지
나는 장래에 이런 상황을 더욱 많이 볼 수 있기를 바란다.나는 내가 현재와 미래의 도서관 작가들에게 가능한 장점을 보여 주었으면 한다.
Reference
이 문제에 관하여(테스트를 애플리케이션의 일부로 만들기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/sobolevn/make-tests-a-part-of-your-app-8nm텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)