说我有一个模型
from pydantic import BaseModel
class Book(BaseModel):
name: str
description: str = Field(min_length=1, max_length=64, pattern="^[a-z]+$")
我想编写单元测试以确保模型已正确定义。 需要明确的是,我相信 pydantic 能够正确验证 is 的定义,我不相信我自己或我的同事能够始终正确定义模型约束,这是我们的业务需求。
为了测试这些约束/要求,我很想用错误的值实例化模型,然后捕获
ValidationError
,然后检查错误的内容以确保它是预期的错误。
比如:
def test_book__error_invalid_description():
with pytest.raise(ValidationError) as err:
Book(name="toto", description="123")
assert "pattern_error" in err.value.errors # not working, that's just the idea
但是我发现这非常丑陋且难以维护。另外,我相信这主要是测试 pydantic 本身以及我的一点要求(甚至没有在这里测试模式要求的所有情况)。
我宁愿测试约束本身是否存在,但我找不到一种干净简洁的方法。 这是我尝试过的:
from annotated_types import MinLen, MaxLen
def test_book__validation_constraints():
assert Book.model_fields["name"].is_required()
assert MinLen(1) in Book.model_fields["description"].metadata
assert MaxLen(64) in Book.model_fields["description"].metadata
assert # no idea how to assert pattern
有更好的方法吗? 您认为测试此类需求是否太过分了?
就我个人而言,我只是通过测试执行该模型的代码来隐式测试这一点。对于我来说,测试这样的序列化器/模型有点过于“单元化”。我在这个方向上的思考很大程度上是由这条思路决定的:https://kentcdodds.com/blog/write-tests
如果是我,假设这是验证 api 请求的正文,我只会编写使用有效和无效值来执行 api 的测试。对我来说,就这些测试真正为您的应用程序添加的内容而言,这是“最物有所值”。
如果您确实想要对模型进行单元测试,那么原始测试(即使用
pytest.raise
)在我看来是正确的方向。第二种方法是测试 pydantic 内部结构。如果 pydantic 改变了他们内部存储这些信息的方式,但模型仍然有效怎么办?您的测试将会中断,但您的代码仍然可以工作。我认为这是一条通往悲伤的黑暗之路,你应该尽量避免。
通常我们希望编写测试来增加我们对应用程序的信心。信心的反面是恐惧。就您而言,我们试图减轻的担心是,您的代码的任何部分依赖于此模型进行验证都将不起作用。假设这是验证 api 请求的正文,“可能发生的坏事”是有人发送请求,并且验证会错误地成功或失败。如果 pydantic 改变了它们的实现,这并不是“坏”。如果验证停止工作那就糟糕了。你的第一种方法在这里成功了,你的第二种方法却没有成功。
对于你的问题“如何确保字符串的最大长度定义为 12,而不创建字符串长度超过 12 的测试用例”:在我看来,你不应该这样做。这里的增值是什么?用长绳子进行测试有什么不好?为了避免这种情况,您要在代码库中添加什么样的复杂性?事实上,增加的复杂性可能会降低我们对应用程序的信心!善待 grug,因为 grug 就是你 https://grugbrain.dev/#grug-on-complexity
测试应该简单,而不是聪明。想想你怕坏什么,直接测试一下。