【问题标题】:Testing for None on a non Optional input parameter在非可选输入参数上测试 None
【发布时间】:2022-10-16 22:36:28
【问题描述】:

假设我有一个具有以下功能的 python 模块:

def is_plontaria(plon: str) -> bool:
    if plon is None:
        raise RuntimeError("None found")

    return plon.find("plontaria") != -1

对于该功能,我有以下单元测试:

def test_is_plontaria_null(self):
    with self.assertRaises(RuntimeError) as cmgr:
        is_plontaria(None)
    self.assertEqual(str(cmgr.exception), "None found")

给定函数中的类型提示,输入参数应始终是已定义的字符串。但是类型提示是……提示。没有什么可以阻止用户传递它想要的任何东西,当先前的操作未能返回预期结果并且未检查这些结果时,None 特别是一个非常常见的选项。

所以我决定在单元测试中测试 None 并检查函数中的输入不是 None 。

问题是:类型检查器(pylance)警告我不应在该调用中使用 None :

Argument of type "None" cannot be assigned to parameter "plon" of type "str" in function "is_plontaria"
  Type "None" cannot be assigned to type "str"

嗯,我已经知道了,这就是测试的目的。

消除该错误的最佳方法是什么?告诉 pylance 在每个测试/文件中忽略这种错误?或者假设传递的参数始终是正确的类型并删除该测试和函数中的 None 检查?

【问题讨论】:

    标签: python python-3.x unit-testing python-unittest pylance


    【解决方案1】:

    您可以在带有注释的特定行上禁用类型检查。

    def test_is_plontaria_null(self):
        with self.assertRaises(RuntimeError) as cmgr:
            is_plontaria(None) # type: ignore
        self.assertEqual(str(cmgr.exception), "None found")
    

    【讨论】:

    • 是的,但是每次禁用警告时我都觉得很脏。这就像作弊一样。鉴于这看起来很常见,我期待一些更优雅的解决方案。但看起来我每次进行这些测试时都必须忽略一堆消息。
    • 因为不想修复错误而禁用警告和因为警告在特定情况下无关紧要而禁用它是有区别的。此外,警告抑制的范围非常有限。
    • 是的,我知道……但感觉是一样的。很高兴看到这是正确的方法:-) 谢谢!
    【解决方案2】:

    这是一个很好的问题。我认为在您的测试中消除该类型错误不是正确的方法。

    不要光顾用户

    虽然我不会说这是普遍正确的做法,但在这种情况下,我会确实建议从is_plontaria 中删除您的None 检查。

    想想你用这个检查完成了什么。假设用户调用is_plontaria(None),即使您使用str 对其进行了注释。如果没有检查,他会导致AttributeError: 'NoneType' object has no attribute 'find' 回溯到return plon.find("plontaria") != -1 行。用户自己想“哎呀,该函数需要str.通过您的检查,他会导致RuntimeError 理想情况下告诉他plon 应该是str

    支票的目的是什么?我不会争辩。无论哪种方式,都会引发错误,因为您的函数被滥用。

    如果用户不小心传递了float 怎么办?还是bool?或者除了str之外的其他任何东西?你想握住用户的手吗每一个参数每一个你写的函数?

    而且我不买“None 是一个特例”的论点。当然,在代码中“躺着”是一种常见的类型,但正如您自己指出的那样,这仍然取决于用户。

    如果您正在使用正确类型的注释代码(正如您应该)并且用户也是,那么这种情况永远不会发生。假设用户有另一个函数foo,他想像这样使用:

    def foo() -> str | None:
        ...
    
    s = foo()
    b = is_plontaria(s)
    

    最后一行应该会导致任何有价值的静态类型检查器引发错误,说is_plontaria 只接受str,但提供了strNone 的联合。甚至大多数 IDE 都将该行标记为有问题。

    用户甚至在运行他的代码之前就应该看到这一点。然后他被迫重新思考,要么改变foo,要么介绍他的自己的在调用函数之前进行类型检查:

    s = foo()
    if isinstance(s, str):
        b = is_plontaria(s)
    else:
        # do something else
    

    预选赛

    公平地说,在某些情况下错误消息非常模糊,并且不能正确地告诉调用者出了什么问题。在这些情况下,介绍您自己的可能会很有用。但除此之外,我总是本着 Python 的精神争辩说,用户应该被认为足够成熟,可以自己做作业。如果他不这样做,那是他的事,而不是你。 (只要你做到了您的家庭作业。)

    可能还有其他情况,提出您自己的类型错误是有意义的,但我认为那些是例外。


    如果必须,请使用Mock

    作为一个小奖励,万一你绝对想要保留该检查并需要在测试中覆盖 if-branch,您可以简单地将 Mock 作为参数传递,前提是您的 if-statement 已调整为检查除 @987654348 以外的任何内容@:

    from unittest import TestCase
    from unittest.mock import Mock
    
    
    def is_plontaria(plon: str) -> bool:
        if not isinstance(plon, str):
            raise RuntimeError("None found")
        return plon.find("plontaria") != -1
    
    
    class Test(TestCase):
        def test_is_plontaria(self) -> None:
            mock_s = Mock()
            with self.assertRaises(RuntimeError):
                is_plontaria(mock_s)
            ...
    

    大多数类型检查器认为Mock 是一种特殊情况,并且不会抱怨它的类型,假设您正在运行测试。例如mypy 对这样的代码非常满意。

    这在其他情况下也很方便。例如,当正在测试的函数需要您的某个自定义类的实例作为其参数时。您显然希望将该函数与该类隔离,因此您可以通过这种方式将模拟传递给它。类型检查器不会介意。

    希望这可以帮助。

    【讨论】:

      猜你喜欢
      • 2012-01-23
      • 1970-01-01
      • 1970-01-01
      • 2018-12-27
      • 1970-01-01
      • 2017-01-04
      • 1970-01-01
      • 2020-12-27
      • 2019-12-14
      相关资源
      最近更新 更多