테스트를 애플리케이션의 일부로 만들기

내 블로그 최초 게시: https://sobolevn.me/2021/02/make-tests-a-part-of-your-app
오늘, 나는 파이썬 사용자를 위해 테스트가 응용 프로그램의 가치 있는 일부분이 되도록 하는 새로운 아이디어를 토론할 것이다.
자, 시작합시다.

현상
현재 원본 코드/테스트 이원론의 현황은 원본 코드를 라이브러리 사용자에게 보내고 테스트를 어떤 방식으로도 포함하지 않는다는 것이다.
때때로 사람들은 tests/ 폴더를 발행판에 추가하기 때문에 만일을 대비하고 있을 뿐이다.대부분의 경우 최종 사용자에게 무용지물이다.
가장 중요한 것은 우리 사용자들이 자신이 이런 상황에 처해 있다는 것을 자주 발견할 수 있다는 것이다. 그들은 반드시 도서관에 특정한 물건을 위해 테스트를 다시 실시해야 한다.
예를 들겠습니다. Django 보기는 권한을 부여받은 사용자만 사용할 수 있습니다.
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse

@login_required
def my_view(request: HttpRequest) -> HttpRespose:
    ...
따라서 테스트에서 최소 두 개의 테스트를 작성해야 합니다.
  • 성공적인 auth 사례 및 비즈니스 논리
  • 실패한 인증 사례
  • 만약 우리가 두 번째를 건너뛰고 기존의 테스트 논리에 의존할 수 있다면 우리는 라이브러리 자체에서 다시 사용할 수 있다. 이것은 신기하지 않습니까?
    이러한 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()
    
    이 가능하다, ~할 수 있다,...
  • 테스트가 허용하지 않는 http 방법
  • 테스트 허용 http 방법
  • 테스트 Cache-Control 제목 및 올바른 값
  • 테스트에서는 무단 사용자
  • 을 허용하지 않습니다.
  • , 다른 사람이 있을지도 몰라요!
  • 당신이 해야 할 일은 녹색 경로를 테스트하는 것입니다. 예를 들어 권한이 부여되지 않은 사용자에게 맞춤형 http 코드를 되돌려주면 특정한 테스트 용례를 맞춤형으로 만들 수 있습니다.
    이 장의 잘못된 점은 논의된 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 이라는 또 다른 훌륭한 도서관에서 왔다.그것은 우리가 정의한 특정한 규칙에 따라 지능적으로 대량의 다른 테스트 데이터를 생성한다.
  • 우리의 예에서 우리는 두 가지 규칙이 있다.제1조 규칙은 int에 정의된 두 개의 def div(a: int, b: int) 매개 변수를 생성한다.두 번째 규칙은 이 정수는 반드시 >= 0에서 정의한 @deal.pre(lambda a, b: a >= 0 and b >= 0)이어야 한다는 것이다.
    우리는 생성된 예시 수량을 제어하고 다른 작은 조정을 진행할 수 있다.
    자세한 내용은 docs을 참조하십시오.
  • 은 왜 ZeroDivisionError은 테스트를 깨지 않았고, Exception은 테스트를 깨지 않았습니까?
    이것이 바로 계약의 작업 원리이기 때문이다. 당신은 모든 가능한 상황을 명확하게 정의했기 때문이다.이상한 일이 생기면 계약이 위반된다.우리의 예시에서 ZeroDivisionErrordeal.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)
  • 및 물성의 법칙: 만약에 ab10bc이면 ac과 같다
  • 우리는 우리의 실시가 이 법률에 부합되는지 확인하기 위해 테스트를 만들 수 있다.어쩌면 우리는 그것을 잊을지도 모른다.아니면 우리 테스트 논리에서 오류가 났거나
    이것이 바로 도서관 작가가 사용자를 고려하고 응용 프로그램으로 테스트를 발표하는 이유이다.
    예를 들어 우리는 인터페이스 정의 자체에 법률을 인코딩할 것이다.
    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 이 실제로 무엇인지, 그리고 그것이 어떤 규칙이 있는지 알고 싶다면, 이 인터페이스들이 그들을 도울 것이다.
    그들 중 대다수는 정의에 부착된 법률이 있다.이것은 우리 사용자들이 가능한 한 적은 절차를 통해 그들의 실현이 정확하다는 것을 확보하는 데 도움이 된다.

    결론
    몇몇 용례에서 테스트를 응용 프로그램과 함께 발표하는 것은 매우 멋진 기능일 수도 있다.
    용례가 정말 달라요!내가 보여준 바와 같이 웹 프레임워크에서 구조 도구와 수학 라이브러리에 이르기까지
    나는 장래에 이런 상황을 더욱 많이 볼 수 있기를 바란다.나는 내가 현재와 미래의 도서관 작가들에게 가능한 장점을 보여 주었으면 한다.

    좋은 웹페이지 즐겨찾기