模拟服务器

基本原理

测试用例的复杂程度各不相同;从测试简单辅助函数的返回值,到渲染整个 Web 客户端以模拟跨多个组件/服务的交互。

在后一种情况下,许多交互将触发服务器请求,否则组件或功能将停止正常运行。然而,重要的是这些请求“不要”到达实际服务器,因为它们可能会影响数据库,这绝对不是测试应该做的事情。

为了克服这个问题,每个请求都应该被拦截并替换为一个用测试(即假)数据模拟实际服务器响应的函数。

由于其中一些请求非常常见(例如 ORM 调用,例如 web_search_readweb_save,或其他方法,例如 get_views),因此默认情况下为每个生成 env 1 的测试实现了模拟服务器。

这些模拟服务器针对每个测试独立运行,可以单独配置,并为 Odoo 中最常用的路由提供开箱即用的帮助程序。

1

一旦环境生成,就需要模拟服务器,因为某些 services 确实会在启动后立即发送服务器请求。

概述

模拟服务器本身实际上非常简单:它是一个对象,包含所有定义的模拟模型的“集合”,以及返回测试数据的路由和回调之间的“映射”。

模拟模型本身包含大部分 CRUD 逻辑,以及用于模拟服务器记录的数据。

一旦模拟服务器启动,它就会劫持“所有”服务器请求,并且对于每个请求,它都会在其“映射”中检查其注册路由之一是否与请求的 URL 匹配。其预定义路由最显着的示例是 /web/dataset/call_kw,它负责在适当的模拟模型上调用 ORM 方法。

注解

与 Hoot 未提供的大多数测试助手一样,可以在 "@web/../tests/web_test_helpers" 模块中找到与模拟服务器相关的助手和类。

配置

默认情况下,模拟服务器是*“空”*,这意味着它没有定义的模拟模型。

但这并不意味着它毫无用处,因为它已经处理了一些预定义的路由,例如负责获取 menustranslations 的路由,这些路由在 env 生成后立即由 services 生成。

但这意味着 ORM 方法将会失败,因为它们所针对的模型尚未定义。

要创建和定义模拟模型,您需要两件事:

  • 扩展 models.Model 类的 class

    • _ 为前缀的特殊键充当元数据持有者,就像在 Python 中一样(例如 _name_order_description 等) 2 3

    • _records 保存代表假记录数据的对象列表;

    • _views 可以是视图类型和 XML 架构的映射;

    • other public class fields 将被解释为字段(通过从 fields 调用适当的方法);

    • 还可以在此处定义特定于模型的方法(例如 "res.users"has_group)。

  • 使用上面定义的类调用 defineModels

2

只有这些特殊键的子集才会产生实际效果。例如,_inherit 将无法按预期工作,更喜欢标准类扩展。

3

这些可以通过每次测试进行更改,而无需考虑清理:对特殊键执行的任何更改都将在测试结束时恢复。

这是一个简单的假 "res.partner" 模型的基本示例:

import { defineModels, fields, models } from "@web/../tests/web_test_helpers";

class ResPartner extends models.Model {
    _name = "res.partner";

    name = fields.Char({ required: true );

    _records = [
        { name: "Mitchel Admin" },
    ];

    _views = {
        form: /* xml */`
            <form>
                <field name="name" />
            </form>
        `,
        list: /* xml */`
            <list>
                <field name="display_name" />
            </list>
        `,
    };
}

defineModels({ ResPartner });

此代码将使这些数据可用于当前测试文件中的*所有*测试。当然,定义一个类并调用 defineModels 也可以在给定测试的*内*完成,以将该模型的范围限制为当前测试。

其他方法如 defineMenusdefineActionsdefineParams 也可用于配置当前模拟服务器。他们的大部分 API 都非常简单(即他们接收类似 JSON 的菜单、操作等描述)。

模拟模型:请求

许多测试用例只需要一个或几个模拟模型即可工作。但有时,要么在模型中实现模拟逻辑太麻烦,要么“路由”(即服务器请求 URL)根本不与 Python 模型关联。

在这种情况下,将调用 onRpc 方法,将路由或 ORM 方法与回调关联起来。

注解

多个 onRpc 调用可以关联​​到同一个路由/ORM方法;在这种情况下,将从最后一个定义到第一个定义顺序调用它们。返回*非空或未定义*值将中断当前链,并返回该值作为服务器请求的最终结果。

它可以通过 4 种不同的方式使用:

onRpc:有路线 ("/")

当第一个参数是以 "/" 开头的 string 时,回调应为 route 回调,接收 Request_ 对象:

onRpc("/route/to/test", async (request) => {
    const { ids }  = await request.json();
    expect.step(ids);
    return {};
});

默认情况下,这些回调的返回值包装在模拟 Response_ 对象的 body 中。

这对于大多数用例来说都很好,但有时回调需要使用具有自定义 statusheadersResponse_ 对象进行响应。

在这种情况下,可选*字典可以作为第三个参数传递,以指定回调是否被视为“纯”*,这意味着其返回值应按原样返回给服务器调用者:

onRpc(
    "/not/found",
    () => new Response("{}", { status: 404 }),
    { pure: true }
);

注解

使用 “纯” 请求回调还可以用于返回除 Response_ 对象之外的任何内容,在这种情况下,返回的值仍将包装在模拟 Response_ 的主体中,以符合 fetch_ / XMLHttpRequest_ API。

onRpc:带有方法名称

当第一个参数是以 "/"strings 列表开头的 string NOT 时,回调应为 ORM 回调,仅当请求的 method 与作为参数给出的匹配时调用。

回调将接收一个包含以下内容的对象:

  • 请求正文中包含的 spread params 值(通常:argskwargsmodelmethod);

  • 一个 parent() 函数,调用该函数时将调用*前面*定义的 ORM 回调;

  • route 键,包含请求的 pathname`(通常:/web/dataset/call_kw`);

  • request 对象。

onRpc("web_read", async ({ args, parent }) => {
    const result = parent();
    expect.step(args[0]); // Contains the list of IDs
    result.some_meta_data = { foo: "bar" };
    return result;
});

onRpc:带有模型名称和方法名称

什么时候:

  • 第一个参数是 string NOT"/"strings 列表开头;

  • 第二个参数也是 stringstrings 列表;

那么回调应该是一个 ORM 回调,仅当请求的 method AND model 与参数中给出的匹配时调用。

它的工作原理与上面的形状相同,但添加了 model 过滤器:

onRpc("web_read", "res.partner", ({ args }) => {
    expect.step(args[0]);
});

onRpc:对于*每个* ORM 方法/模型

only 参数是回调时,它应该是为 每个 ORM 调用调用的 ORM 回调:

onRpc(({ method }) => {
    expect.step(method); // Will step every ORM method call on every model
});

模拟模型:字段

模型字段可以通过两种方式声明:

  • 作为 public class fields

  • _fields 特殊键下。例如:

    test("test view with date fields", async () => {
        // `_fields` can be assigned over, or extended directly.
        ResPartner._fields.date = fields.Date({ string: "Registration date" });
    });
    

字段构造函数可以使用参数字典来指示它们的行为。其中一些字段(例如关系字段)需要 relation 属性才能正常工作。

与实际的 Python 服务器字段相比,模拟字段可以执行的操作存在限制,但期望支持最基本的属性:readonlyrequiredstring 等。

computerelated 确实适用于最基本的用例,但不要指望它们能够像在实际服务器上那样可靠地运行。

注解

为每个创建的模型预定义了 4 个默认字段:iddisplay_namecreated_atupdated_at。它们的行为与服务器端对应项相匹配(例如 id 是增量的,而 display_name 具有与其服务器端对应项类似的 compute 函数),并且可以在需要时被覆盖。

模拟模型:记录

模型加载时,模型记录是根据 _records 特殊键中包含的每个对象生成的。它们根据当前模型上可用的字段进行验证;如果属性与模型上定义的字段不匹配,则会引发错误。

重要

_records *在模型加载后(即模拟服务器启动后)*不能*更改。该密钥仅用于生成初始记录。如果应该在模型创建之后添加记录,请通过 UI 中的可用组件或通过模拟服务器实例上的直接 ORM 调用来添加记录。

模拟模型:视图

由于实际视图需要声明 "ir.ui.view" 模型,因此模拟模型使用简化的*映射*来提供视图拱门。

_view 特殊键是一个字典,其 keys 是视图类型,可选地附有视图 ID,其 values 是 XML arch 字符串表示形式。

默认情况下,视图 ID 为 false,但可以使用结合视图类型及其 ID 的逗号分隔键显式指定:

// Will simulate a list view with no ID (false).
ResPartner._views.list = /* xml */ `
    <list>
        <field name="display_name" />
    </list>
`;

// Will simulate a form view with ID 418.
ResPartner._views["form,418"] = /* xml */ `
    <form>
        <field name="name" />
        <field name="date" />
    </form>
`;

生成模拟服务器

就像在大多数情况下一样,对于给定的测试只有一台服务器可以处于活动状态。

如上所述,创建 env 将自动部署模拟服务器。

这意味着所有这些方法也将创建一个模拟服务器,因为它们确实创建了一个 env

  • makeMockEnv

  • mountWithCleanup);

  • mountView)。

然而,一些低级功能可能需要在*没有*环境的情况下生成模拟服务器。为此,可以单独调用 makeMockServer 帮助程序来启动模拟服务器。

注解

makeMockServer 应该“仅”由低级功能使用,例如在没有环境的情况下测试 rpc 函数。它并不意味着用作检索当前模拟服务器实例的方法。为此,请参阅 MockServer.current

注解

需要注意的是,模拟服务器启动后对 makeMockServer 的后续调用将被忽略。

与服务器交互

虽然大多数服务器交互预计由测试用例中生成的生产代码直接或间接完成,但有时绕过 UI 并直接调用模拟服务器是有意义的(例如,模拟另一个用户在其他地方以某种方式更改了数据库)。

这可以通过检索包含当前模拟服务器实例的 MockServer.current 静态属性来完成(仅在初始化之后):

// Most common ORM methods are provided out of the box by server models,
// and are synchronous. Although, be careful that this will NOT trigger a
// UI re-render, and will ONLY affect the (fake) database.
const ids = MockServer.env["res.partner"].create([
    { name: "foo" },
    { name: "bar" },
]);

小技巧

MockServer.env 只是 MockServer.current.env 的快捷方式。