Assert对象类型注解的Python单元测试

3xiyfsfu  于 2023-02-01  发布在  Python
关注(0)|答案(2)|浏览(143)

在Python 3.11版以下的版本中,assert_type(源代码)不可用,如何通过unittestTestCase类Assert类型注解?

from typing import List
from unittest import TestCase

def dummy() -> List[str]:
    return ["one", "two", "three"]

class TestDummy(TestCase):

    def test_dummy(self):
        self.assertEqual(
            List[str],
            type(dummy())
        )

测试失败,输出如下:

<class 'list'> != typing.List[str]

Expected :typing.List[str]
Actual   :<class 'list'>
<Click to see difference>

Traceback (most recent call last):
  File "C:\Users\z\dev\mercata\scratch\mock_testing.py", line 12, in test_dummy
    self.assertEqual(
AssertionError: typing.List[str] != <class 'list'>

我目前使用的方法如下:

data = dummy()
self.assertTrue(
    type(data) == list
)
self.assertTrue(all([
    type(d) == str for d in data
]))

这是可行的,但是需要迭代整个对象,这对于更大的数据集来说很难处理,对于Python 3.11以下的版本(不需要第三方包),有没有更有效的方法?

oxf4rvwz

oxf4rvwz1#

assert_type用于请求一个静态类型检查器来确认一个值是某个类型。在正常运行时,这个方法不做任何事情。如果你想使用它,那么你应该使用静态分析工具,例如mypy或pyright。检查assertEqual是一个运行时操作,并且不像某些语言,python中泛型的示例在运行时不保留它们的类型信息。这就是为什么类被显示为标准的<class 'list'>,而不是来自方法类型注解的泛型。
因为assert_type在运行时不执行任何操作,所以它不会检查实际列表的内容。它被用来向代码中添加显式类型检查,并且只有在所有关于变量构造方式的输入都经过了正确的类型检查时才有用。因此,它在单元测试中也没有用。
例如,以下脚本只生成一个错误:

from typing import assert_type

def dummy() -> list[str]:
    return [1]

res = dummy()
assert_type(res, list[str])
(venv) $ mypy test.py
test.py:4: error: List item 0 has incompatible type "int"; expected "str"  [list-item]
Found 1 error in 1 file (checked 1 source file)

这检测到dummy返回的int列表的错误,但是assert_type成功了,因为如果dummy遵守了它的约定,它将是正确的。
如果我们像下面这样修复dummy,那么此时我们会得到预期的assert_type错误:
一个二个一个一个

w9apscun

w9apscun2#

虽然我同意评论者表达的一般观点,即这类事情可能应该留给静态类型检查器而不是单元测试,但出于学术目的,您可以构造自己的Assert,而无需太多努力。
list[str]是泛型类型list的指定版本。通过为list添加下标,实际上是调用它的__class_getitem__方法,该方法返回指定的类型。类型参数 * 实际上是 * 存储的,类型模块提供了get_args/get_origin函数,以便在运行时从泛型类型中提取更详细的类型信息。

from typing import get_args

print(get_args(list[str]))  # (<class 'str'>,)

问题在于任何具体的list对象(比如["one", "two", "three"])都不存储任何关于它所包含的元素类型的信息(原因显而易见),这意味着在运行时,我们必须自己检查元素的类型。
因此问题就变成了你希望你的检查有多迂腐。例如,列表可以是你想要的长度(或者你的内存允许的长度)。如果你有一个一百万个元素的列表对象,你真的想检查每一个元素吗?一个可能的折衷方案可能是只检查第一个元素的类型或者类似的东西。
下面是一个函数的例子,它检查仅由“常规”类型参数化的任意可迭代类型(即不是类似list[tuple[int]]的类型):

from collections.abc import Iterable
from types import GenericAlias
from typing import Union, cast, get_origin, get_args

def is_of_iter_type(
    value: object,
    type_: Union[type[Iterable[object]], GenericAlias],
    pedantic: bool = False,
) -> bool:
    if isinstance(type_, type):  # something like unspecified `list`
        return isinstance(value, type_)
    if isinstance(type_, GenericAlias):  # a specified generic like `list[str]`
        origin, args = get_origin(type_), get_args(type_)
        if not isinstance(origin, type) or not issubclass(origin, Iterable):
            raise TypeError
        arg = cast(type, args[0])
        if not isinstance(arg, type):  # type arg is a type var or another generic alias
            raise TypeError
        if not isinstance(value, origin):
            return False
        if pedantic:
            return all(isinstance(item, arg) for item in value)
        else:
            return isinstance(next(iter(value)), arg)
    raise TypeError

还要注意的是,根据你实际传递给这个函数的可迭代对象的不同,(试图)消耗结果迭代器(通过nextall)可能是一个糟糕的主意,你需要确保这不会产生任何不良的副作用。
下面是一个演示:

print(is_of_iter_type("a", list[str]))  # False
print(is_of_iter_type(["a"], list[str]))  # True
print(is_of_iter_type(["a"], list))  # True
print(is_of_iter_type(["a", 1], list[str]))  # True
print(is_of_iter_type(["a", 1], list[str], pedantic=True))  # False

要将其合并到unittest.TestCase中,可以执行以下操作:

...
from unittest import TestCase

class ExtendedTestCase(TestCase):
    def assert_is_of_iter_type(
        self,
        value: object,
        type_: Union[type[Iterable[object]], GenericAlias],
        pedantic: bool = False,
    ) -> None:
        if not is_of_iter_type(value, type_, pedantic=pedantic):
            self.fail(f"{value} is not of type {type_}")

    def test(self) -> None:
        self.assert_is_of_iter_type(["a", 1], list[str], pedantic=True)

但同样,这很可能不是一个好主意,因为像--strict模式下的mypy这样的东西在确保整个代码的类型安全方面可能比您希望在运行时做得更好。这意味着如果您声明def dummy() -> list[str]: ...,但在函数体中您声明了return ["a", 1],那么mypy将接收到它并向您大喊大叫。因此,则不需要这样的测试。

相关问题