python-3.x @ singletispatch和@typechecked的PyTest未引发预期错误

mec1mxoz  于 2023-08-08  发布在  Python
关注(0)|答案(1)|浏览(103)

目标:成功通过test_score_not_implemented_error()NotImplementedError的测试用例。
@singledispatchdef score()的目的是在count_negcount_pos的参数不匹配Tuple[int, int]Tuple[List[int], List[int]]时引发NotImplementedError
我想通过测试此异常处理。test_score_not_implemented_error()
然而,出乎意料的是,我在其他多态函数上实现了@typechecked,却得到了一个错误。
我对我需要多态函数的方法很有信心,我的测试函数有合适的测试用例。我怀疑问题在于我如何实现def score()的多态函数。
Tweak:从多态函数中删除@typechecked抛出:

FAILED tests/test_tps.py::test_score_not_implemented_error[0-01] - TypeError: can only concatenate str (not "int") to str
FAILED tests/test_tps.py::test_score_not_implemented_error[count_neg3-count_pos3] - TypeError: unsupported operand type(s) for +: 'int' and 'str'
FAILED tests/test_tps.py::test_score_not_implemented_error[count_neg4-count_pos4] - TypeError: unsupported operand type(s) for +: 'int' and 'str'
FAILED tests/test_tps.py::test_score_not_implemented_error[count_neg5-count_pos5] - TypeError: unsupported operand type(s) for +: 'int' and 'str'

字符串
tests/test_score.py

from typeguard import typechecked
from functools import singledispatch

import pytest
from pytest_cases import parametrize
from typing import Any, List, Tuple, Type, Union

@singledispatch
def score(count_neg: Any, count_pos: Any) -> None:
    raise NotImplementedError(f'{type(count_neg)} and or {type(count_pos)} are not supported.')

@score.register(int)
@typechecked
def score_int(count_neg: int, count_pos: int) -> float:
    return round(100 * count_pos / (count_pos + count_neg), 1)

@score.register(list)
@typechecked
def score_list(count_neg: List[int], count_pos: List[int]) -> float:
    return round(100 * sum(count_pos) / (sum(count_pos) + sum(count_neg)), 1)

@parametrize('count_neg, count_pos',
             [('0', 0),
              (0, '0'),
              ('0', '0'),
              (['0'], [0]),
              ([0], ['0']),
              (['0'], ['0']),
              (None, None)])
def test_score_not_implemented_error(count_neg: Union[str, int, List[str], List[int], None],
                                                       count_pos: Union[str, int, List[str], List[int], None],
                                                       error: Type[BaseException] = NotImplementedError):
    with pytest.raises(error) as exc_info:
        score(count_neg, count_pos)

    assert exc_info.type is error


回溯:

(venv) me@laptop:~/BitBucket/project $ python -m pytest tests/test_score.py 
===================================================================================================================== test session starts =====================================================================================================================
platform linux -- Python 3.9.16, pytest-7.4.0, pluggy-1.0.0
rootdir: /home/danielbell/BitBucket/pdl1-lung
plugins: hydra-core-1.3.2, typeguard-3.0.2, mock-3.11.1, cases-3.6.13, dvc-3.2.3, anyio-3.5.0
collected 7 items                                                                                                                                                                                                                                             

tests/test_tps.py .F.FFF.                                                                                                                                                                                                                               [100%]

========================================================================================================================== FAILURES ===========================================================================================================================
___________________________________________________________________________________________________________ test_score_not_implemented_error[0-01] ____________________________________________________________________________________________________________

count_neg = 0, count_pos = '0', error = <class 'NotImplementedError'>

    @parametrize('count_neg, count_pos',
                 [('0', 0),
                  (0, '0'),
                  ('0', '0'),
                  (['0'], [0]),
                  ([0], ['0']),
                  (['0'], ['0']),
                  (None, None)])
    def test_score_not_implemented_error(count_neg: Union[str, int, List[str], List[int], None],
                                                           count_pos: Union[str, int, List[str], List[int], None],
                                                           error: Type[BaseException] = NotImplementedError):
        with pytest.raises(error) as exc_info:
>           score(count_neg, count_pos)

tests/test_tps.py:38: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../miniconda3/envs/pdl1lung/lib/python3.9/functools.py:888: in wrapper
    return dispatch(args[0].__class__)(*args, **kw)
tests/test_tps.py:16: in score_int
    def score_int(count_neg: int, count_pos: int) -> float:
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_functions.py:113: in check_argument_types
    check_type_internal(value, expected_type, memo=memo)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

value = '0', annotation = <class 'int'>, memo = <typeguard.CallMemo object at 0x7f591213ccc0>

    def check_type_internal(value: Any, annotation: Any, memo: TypeCheckMemo) -> None:
        """
        Check that the given object is compatible with the given type annotation.
    
        This function should only be used by type checker callables. Applications should use
        :func:`~.check_type` instead.
    
        :param value: the value to check
        :param annotation: the type annotation to check against
        :param memo: a memo object containing configuration and information necessary for
            looking up forward references
        """
    
        if isinstance(annotation, ForwardRef):
            try:
                annotation = evaluate_forwardref(annotation, memo)
            except NameError:
                if global_config.forward_ref_policy is ForwardRefPolicy.ERROR:
                    raise
                elif global_config.forward_ref_policy is ForwardRefPolicy.WARN:
                    warnings.warn(
                        f"Cannot resolve forward reference {annotation.__forward_arg__!r}",
                        TypeHintWarning,
                        stacklevel=get_stacklevel(),
                    )
    
                return
    
        if annotation is Any or annotation is SubclassableAny or isinstance(value, Mock):
            return
    
        # Skip type checks if value is an instance of a class that inherits from Any
        if not isclass(value) and SubclassableAny in type(value).__bases__:
            return
    
        extras: tuple[Any, ...]
        origin_type = get_origin(annotation)
        if origin_type is Annotated:
            annotation, *extras_ = get_args(annotation)
            extras = tuple(extras_)
            origin_type = get_origin(annotation)
        else:
            extras = ()
    
        if origin_type is not None:
            args = get_args(annotation)
    
            # Compatibility hack to distinguish between unparametrized and empty tuple
            # (tuple[()]), necessary due to https://github.com/python/cpython/issues/91137
            if origin_type in (tuple, Tuple) and annotation is not Tuple and not args:
                args = ((),)
        else:
            origin_type = annotation
            args = ()
    
        for lookup_func in checker_lookup_functions:
            checker = lookup_func(origin_type, args, extras)
            if checker:
                checker(value, origin_type, args, memo)
                return
    
        if not isinstance(value, origin_type):
>           raise TypeCheckError(f"is not an instance of {qualified_name(origin_type)}")
E           typeguard.TypeCheckError: argument "count_pos" (str) is not an instance of int

../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:680: TypeCheckError
___________________________________________________________________________________________________ test_score_not_implemented_error[count_neg3-count_pos3] ___________________________________________________________________________________________________

count_neg = ['0'], count_pos = [0], error = <class 'NotImplementedError'>

    @parametrize('count_neg, count_pos',
                 [('0', 0),
                  (0, '0'),
                  ('0', '0'),
                  (['0'], [0]),
                  ([0], ['0']),
                  (['0'], ['0']),
                  (None, None)])
    def test_score_not_implemented_error(count_neg: Union[str, int, List[str], List[int], None],
                                                           count_pos: Union[str, int, List[str], List[int], None],
                                                           error: Type[BaseException] = NotImplementedError):
        with pytest.raises(error) as exc_info:
>           score(count_neg, count_pos)

tests/test_tps.py:38: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../miniconda3/envs/pdl1lung/lib/python3.9/functools.py:888: in wrapper
    return dispatch(args[0].__class__)(*args, **kw)
tests/test_tps.py:22: in score_list
    def score_list(count_neg: List[int], count_pos: List[int]) -> float:
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_functions.py:113: in check_argument_types
    check_type_internal(value, expected_type, memo=memo)
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:676: in check_type_internal
    checker(value, origin_type, args, memo)
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:273: in check_list
    check_type_internal(v, args[0], memo)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

value = '0', annotation = <class 'int'>, memo = <typeguard.CallMemo object at 0x7f5854383770>

    def check_type_internal(value: Any, annotation: Any, memo: TypeCheckMemo) -> None:
        """
        Check that the given object is compatible with the given type annotation.
    
        This function should only be used by type checker callables. Applications should use
        :func:`~.check_type` instead.
    
        :param value: the value to check
        :param annotation: the type annotation to check against
        :param memo: a memo object containing configuration and information necessary for
            looking up forward references
        """
    
        if isinstance(annotation, ForwardRef):
            try:
                annotation = evaluate_forwardref(annotation, memo)
            except NameError:
                if global_config.forward_ref_policy is ForwardRefPolicy.ERROR:
                    raise
                elif global_config.forward_ref_policy is ForwardRefPolicy.WARN:
                    warnings.warn(
                        f"Cannot resolve forward reference {annotation.__forward_arg__!r}",
                        TypeHintWarning,
                        stacklevel=get_stacklevel(),
                    )
    
                return
    
        if annotation is Any or annotation is SubclassableAny or isinstance(value, Mock):
            return
    
        # Skip type checks if value is an instance of a class that inherits from Any
        if not isclass(value) and SubclassableAny in type(value).__bases__:
            return
    
        extras: tuple[Any, ...]
        origin_type = get_origin(annotation)
        if origin_type is Annotated:
            annotation, *extras_ = get_args(annotation)
            extras = tuple(extras_)
            origin_type = get_origin(annotation)
        else:
            extras = ()
    
        if origin_type is not None:
            args = get_args(annotation)
    
            # Compatibility hack to distinguish between unparametrized and empty tuple
            # (tuple[()]), necessary due to https://github.com/python/cpython/issues/91137
            if origin_type in (tuple, Tuple) and annotation is not Tuple and not args:
                args = ((),)
        else:
            origin_type = annotation
            args = ()
    
        for lookup_func in checker_lookup_functions:
            checker = lookup_func(origin_type, args, extras)
            if checker:
                checker(value, origin_type, args, memo)
                return
    
        if not isinstance(value, origin_type):
>           raise TypeCheckError(f"is not an instance of {qualified_name(origin_type)}")
E           typeguard.TypeCheckError: item 0 of argument "count_neg" (list) is not an instance of int

../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:680: TypeCheckError
___________________________________________________________________________________________________ test_score_not_implemented_error[count_neg4-count_pos4] ___________________________________________________________________________________________________

count_neg = [0], count_pos = ['0'], error = <class 'NotImplementedError'>

    @parametrize('count_neg, count_pos',
                 [('0', 0),
                  (0, '0'),
                  ('0', '0'),
                  (['0'], [0]),
                  ([0], ['0']),
                  (['0'], ['0']),
                  (None, None)])
    def test_score_not_implemented_error(count_neg: Union[str, int, List[str], List[int], None],
                                                           count_pos: Union[str, int, List[str], List[int], None],
                                                           error: Type[BaseException] = NotImplementedError):
        with pytest.raises(error) as exc_info:
>           score(count_neg, count_pos)

tests/test_tps.py:38: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../miniconda3/envs/pdl1lung/lib/python3.9/functools.py:888: in wrapper
    return dispatch(args[0].__class__)(*args, **kw)
tests/test_tps.py:22: in score_list
    def score_list(count_neg: List[int], count_pos: List[int]) -> float:
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_functions.py:113: in check_argument_types
    check_type_internal(value, expected_type, memo=memo)
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:676: in check_type_internal
    checker(value, origin_type, args, memo)
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:273: in check_list
    check_type_internal(v, args[0], memo)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

value = '0', annotation = <class 'int'>, memo = <typeguard.CallMemo object at 0x7f58540d45e0>

    def check_type_internal(value: Any, annotation: Any, memo: TypeCheckMemo) -> None:
        """
        Check that the given object is compatible with the given type annotation.
    
        This function should only be used by type checker callables. Applications should use
        :func:`~.check_type` instead.
    
        :param value: the value to check
        :param annotation: the type annotation to check against
        :param memo: a memo object containing configuration and information necessary for
            looking up forward references
        """
    
        if isinstance(annotation, ForwardRef):
            try:
                annotation = evaluate_forwardref(annotation, memo)
            except NameError:
                if global_config.forward_ref_policy is ForwardRefPolicy.ERROR:
                    raise
                elif global_config.forward_ref_policy is ForwardRefPolicy.WARN:
                    warnings.warn(
                        f"Cannot resolve forward reference {annotation.__forward_arg__!r}",
                        TypeHintWarning,
                        stacklevel=get_stacklevel(),
                    )
    
                return
    
        if annotation is Any or annotation is SubclassableAny or isinstance(value, Mock):
            return
    
        # Skip type checks if value is an instance of a class that inherits from Any
        if not isclass(value) and SubclassableAny in type(value).__bases__:
            return
    
        extras: tuple[Any, ...]
        origin_type = get_origin(annotation)
        if origin_type is Annotated:
            annotation, *extras_ = get_args(annotation)
            extras = tuple(extras_)
            origin_type = get_origin(annotation)
        else:
            extras = ()
    
        if origin_type is not None:
            args = get_args(annotation)
    
            # Compatibility hack to distinguish between unparametrized and empty tuple
            # (tuple[()]), necessary due to https://github.com/python/cpython/issues/91137
            if origin_type in (tuple, Tuple) and annotation is not Tuple and not args:
                args = ((),)
        else:
            origin_type = annotation
            args = ()
    
        for lookup_func in checker_lookup_functions:
            checker = lookup_func(origin_type, args, extras)
            if checker:
                checker(value, origin_type, args, memo)
                return
    
        if not isinstance(value, origin_type):
>           raise TypeCheckError(f"is not an instance of {qualified_name(origin_type)}")
E           typeguard.TypeCheckError: item 0 of argument "count_pos" (list) is not an instance of int

../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:680: TypeCheckError
___________________________________________________________________________________________________ test_score_not_implemented_error[count_neg5-count_pos5] ___________________________________________________________________________________________________

count_neg = ['0'], count_pos = ['0'], error = <class 'NotImplementedError'>

    @parametrize('count_neg, count_pos',
                 [('0', 0),
                  (0, '0'),
                  ('0', '0'),
                  (['0'], [0]),
                  ([0], ['0']),
                  (['0'], ['0']),
                  (None, None)])
    def test_score_not_implemented_error(count_neg: Union[str, int, List[str], List[int], None],
                                                           count_pos: Union[str, int, List[str], List[int], None],
                                                           error: Type[BaseException] = NotImplementedError):
        with pytest.raises(error) as exc_info:
>           score(count_neg, count_pos)

tests/test_tps.py:38: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../miniconda3/envs/pdl1lung/lib/python3.9/functools.py:888: in wrapper
    return dispatch(args[0].__class__)(*args, **kw)
tests/test_tps.py:22: in score_list
    def score_list(count_neg: List[int], count_pos: List[int]) -> float:
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_functions.py:113: in check_argument_types
    check_type_internal(value, expected_type, memo=memo)
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:676: in check_type_internal
    checker(value, origin_type, args, memo)
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:273: in check_list
    check_type_internal(v, args[0], memo)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

value = '0', annotation = <class 'int'>, memo = <typeguard.CallMemo object at 0x7f58541444f0>

    def check_type_internal(value: Any, annotation: Any, memo: TypeCheckMemo) -> None:
        """
        Check that the given object is compatible with the given type annotation.
    
        This function should only be used by type checker callables. Applications should use
        :func:`~.check_type` instead.
    
        :param value: the value to check
        :param annotation: the type annotation to check against
        :param memo: a memo object containing configuration and information necessary for
            looking up forward references
        """
    
        if isinstance(annotation, ForwardRef):
            try:
                annotation = evaluate_forwardref(annotation, memo)
            except NameError:
                if global_config.forward_ref_policy is ForwardRefPolicy.ERROR:
                    raise
                elif global_config.forward_ref_policy is ForwardRefPolicy.WARN:
                    warnings.warn(
                        f"Cannot resolve forward reference {annotation.__forward_arg__!r}",
                        TypeHintWarning,
                        stacklevel=get_stacklevel(),
                    )
    
                return
    
        if annotation is Any or annotation is SubclassableAny or isinstance(value, Mock):
            return
    
        # Skip type checks if value is an instance of a class that inherits from Any
        if not isclass(value) and SubclassableAny in type(value).__bases__:
            return
    
        extras: tuple[Any, ...]
        origin_type = get_origin(annotation)
        if origin_type is Annotated:
            annotation, *extras_ = get_args(annotation)
            extras = tuple(extras_)
            origin_type = get_origin(annotation)
        else:
            extras = ()
    
        if origin_type is not None:
            args = get_args(annotation)
    
            # Compatibility hack to distinguish between unparametrized and empty tuple
            # (tuple[()]), necessary due to https://github.com/python/cpython/issues/91137
            if origin_type in (tuple, Tuple) and annotation is not Tuple and not args:
                args = ((),)
        else:
            origin_type = annotation
            args = ()
    
        for lookup_func in checker_lookup_functions:
            checker = lookup_func(origin_type, args, extras)
            if checker:
                checker(value, origin_type, args, memo)
                return
    
        if not isinstance(value, origin_type):
>           raise TypeCheckError(f"is not an instance of {qualified_name(origin_type)}")
E           typeguard.TypeCheckError: item 0 of argument "count_neg" (list) is not an instance of int

../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/typeguard/_checkers.py:680: TypeCheckError
====================================================================================================================== warnings summary =======================================================================================================================
../../miniconda3/envs/pdl1lung/lib/python3.9/site-packages/cytomine/models/collection.py:26
  /home/danielbell/miniconda3/envs/pdl1lung/lib/python3.9/site-packages/cytomine/models/collection.py:26: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.10 it will stop working
    from collections import MutableSequence

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=================================================================================================================== short test summary info ===================================================================================================================
FAILED tests/test_tps.py::test_score_not_implemented_error[0-01] - typeguard.TypeCheckError: argument "count_pos" (str) is not an instance of int
FAILED tests/test_tps.py::test_score_not_implemented_error[count_neg3-count_pos3] - typeguard.TypeCheckError: item 0 of argument "count_neg" (list) is not an instance of int
FAILED tests/test_tps.py::test_score_not_implemented_error[count_neg4-count_pos4] - typeguard.TypeCheckError: item 0 of argument "count_pos" (list) is not an instance of int
FAILED tests/test_tps.py::test_score_not_implemented_error[count_neg5-count_pos5] - typeguard.TypeCheckError: item 0 of argument "count_neg" (list) is not an instance of int
=========================================================================================================== 4 failed, 3 passed, 1 warning in 0.94s ============================================================================================================

h7wcgrx3

h7wcgrx31#

问题是,您期望所有的测试都会引发一个NotImplementedError,但在某些情况下,您传递的参数类型不正确,因此您得到的是TypeCheckError。让我们看看你的测试案例。

  • ('0', 0)--通过

这会引发一个NotImplementedError,因为除了第一个参数中的字符串之外,没有score方法。

  • (0, '0')--未通过

这在TypeCheckError中失败,因为score_int需要(int, int),但您传递的是(int, str)

  • ('0', '0')--通过

这会引发一个NotImplementedError,因为除了第一个参数中的字符串之外,没有score方法。

  • (['0'], [0])-未通过

这会引发一个TypeCheckError,因为score_list需要list[int],但您已经传递了一个list[str]

  • ([0], ['0'])--未通过

此测试失败的原因与上一测试相同,但问题是count_pos参数而不是count_neg

  • (['0'], ['0'])--未通过

这与前两次测试的原因相同。

  • (None, None)])--通过

这是因为没有score方法接受None作为参数类型。
为了让这个测试通过所有的测试用例,你还需要参数化error。可能是这样的:

from typeguard import typechecked, TypeCheckError
from functools import singledispatch

import pytest
from typing import Any, Type

@singledispatch
def score(count_neg: Any, count_pos: Any) -> Any:
    raise NotImplementedError(
        f"{type(count_neg)} and or {type(count_pos)} are not supported."
    )

@score.register(int)
@typechecked
def score_int(count_neg: int, count_pos: int) -> float:
    return round(100 * count_pos / (count_pos + count_neg), 1)

@score.register(list)
@typechecked
def score_list(count_neg: list[int], count_pos: list[int]) -> float:
    return round(100 * sum(count_pos) / (sum(count_pos) + sum(count_neg)), 1)

@pytest.mark.parametrize(
    "count_neg, count_pos, error",
    [
        ("0", 0, NotImplementedError),
        (0, "0", TypeCheckError),
        ("0", "0", NotImplementedError),
        (["0"], [0], TypeCheckError),
        ([0], ["0"], TypeCheckError),
        (["0"], ["0"], TypeCheckError),
        (None, None, NotImplementedError),
    ],
)
def test_score_not_implemented_error(
    count_neg: str | int | list[str] | list[int] | None,
    count_pos: str | int | list[str] | list[int] | None,
    error: Type[BaseException],
):
    with pytest.raises(error):
        score(count_neg, count_pos)

字符串
请注意,我做了以下更改:

  • 我使用的是pytest内置的parametrize装饰器,而不是pytest_cases中的装饰器。
  • 我正在使用更现代的类型注解。
  • 我修改了初始score定义,返回Any而不是None;这防止了其它方法的键入错误。
  • 我已经从测试中删除了assert,因为这是由with pytest.raises()逻辑隐式执行的(如果它引发了一个不同于预期的异常,测试将失败)。

相关问题