=========== Mock server =========== Rationale ========= Test cases can range in complexity; from testing the return value of a simple helper function, to rendering an entire webclient to simulate interactions across several components/services. In the latter cases, many interactions will trigger server requests, without which the components or features will stop functioning properly. However, it is important that these requests *do not* land on the actual server, as they could affect the database, which is definitely not something a test should be doing. To overcome this, each request should be intercepted and replaced by a function emulating actual server responses with test (i.e. fake) data. Since some of these requests are very common (e.g. ORM calls, such as `web_search_read` or `web_save`, or other methods such as `get_views`), a mock server has been implemented by default for every test that spawns an :abbr:`env (Odoo environment)` [#]_. These mock servers act independently for each test, can be configured separately, and provide out of the box helpers for the most used routes in Odoo. .. [#] A mock server is needed as soon as an environment is spawned because some :doc:`services <../services>` do send server requests as soon as they start. Overview ======== A mock server is actually quite simple in itself: it is an object containing a *collection* of all defined mock models, and a *mapping* between routes and callbacks returning the test data. The mock models themselves hold most of the CRUD logic, as well as the data used to simulate server records. Once a mock server starts, it hijacks *all* server requests, and for each of them it will check in its *mapping* whether one of its registered routes matches the requested URL. The most notable example of its pre-defined routes is `/web/dataset/call_kw`, which is responsible for calling an ORM method on the appropriate mock model. .. note:: Like most test helpers not provided by Hoot, mock server-related helpers and classes can be found in the `"@web/../tests/web_test_helpers"` module. .. _mock-server/configuration: Configuration ============= By default, a mock server is *"empty"*, meaning that it has no defined mock model. This does not mean that it is useless though, as it will already handle a few pre-defined routes, such as the ones responsible for fetching `menus` and `translations`, which are spawned by :doc:`services <../services>` as soon as an `env` is spawned. But this means that ORM methods will fail, as the model that they target has not been defined yet. To create and define a mock model, you need 2 things: - a `class` extending the `models.Model` class; - special keys prefixed with a `_` act as metadata holders, like in Python (e.g. `_name`, `_order`, `_description`, etc.) [#]_ [#]_; - `_records` holds the list of objects representing fake record data; - `_views` can be a mapping of view types and XML arches; - other `public class fields `_ will be interpreted as fields (by calling the appropriate method from `fields`); - model-specific methods (such as `has_group` for `"res.users"`) can also be defined here. - calling `defineModels` with the class defined above. .. [#] Only a subset of these special keys will have an actual effect. For example, `_inherit` will not work as intended, prefer standard class extension. .. [#] These can be altered by each test without thinking about cleaning up: any change performed on a special key will be reverted at the end of a test. Here is a basic example of a simple, fake, `"res.partner"` model: .. code-block:: javascript 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 */`
`, list: /* xml */` `, }; } defineModels({ ResPartner }); This code will make these data available for *all* tests in the current test file. Of course, defining a class and calling `defineModels` can also be done from *within* a given test to limit the scope of that model to the current test. Other methods such as `defineMenus`, `defineActions` or `defineParams` can also be used to configure the current mock server. Most of their API is quite straightforward (i.e. they receive JSON-like descriptions of menus, actions, etc.). Mock models: requests --------------------- Many test cases only require one or a few mock models to work. But sometimes, it is either too bothersome to implement the mocking logic within a model, or a *route* (i.e. server request URL) is simply not associated to a Python model at all. In such cases, the `onRpc` method is to be called, to associate a route or an ORM method to a callback. .. note:: Multiple `onRpc` calls can be associated to the same route / ORM method; in which case they will be called sequentially from last to first defined. Returning a *non-null-or-undefined* value will interrupt the current chain, and return that value as final result of the server request. It can be used in 4 different ways: `onRpc`: with a route (`"/"`) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When the first argument is a `string` starting with a `"/"`, the callback is expected to be a *route* callback, receiving a `Request_` object: .. code-block:: javascript onRpc("/route/to/test", async (request) => { const { ids } = await request.json(); expect.step(ids); return {}; }); By default, the return value of these callbacks are wrapped within the `body` of a mock `Response_` object. This is fine for most use-cases, but sometimes the callback needs to respond with a `Response_` object with custom `status` or `headers`. In such cases, an *optional* dictionary can be passed as a 3rd argument to specify whether the callback is to be considered *"pure"*, meaning that its return value should be returned as-is to the server caller: .. code-block:: javascript onRpc( "/not/found", () => new Response("{}", { status: 404 }), { pure: true } ); .. note:: Using *"pure"* request callbacks can also be used to return anything else than a `Response_` object, in which case the returned value will still be wrapped in the body of a mock `Response_` to comply with the `fetch_` / `XMLHttpRequest_` APIs. `onRpc`: with method name(s) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When the first argument is a `string` *NOT* starting with a `"/"` or a list of `strings`, the callback is expected to be an ORM callback, only called when the request's `method` matches the one given as argument. The callback will receive an object containing: - the *spread* `params` value contained in the request body (typically: `args`, `kwargs`, `model` and `method`); - a `parent()` function, which when invoked will call the defined ORM callback *preceding* this one; - a `route` key, containing the `pathname` of the request (typically: `/web/dataset/call_kw`); - the `request` object. .. code-block:: javascript 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`: with model name(s) AND method name(s) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When: - the first argument is a `string` *NOT* starting with a `"/"` or a list of `strings`; - the second argument is also a `string` or a list of `strings`; Then the callback is expected to be an ORM callback, only called when the request's `method` *AND* `model` match the ones given in the arguments. This works just the same as the above shape, with an added `model` filter: .. code-block:: javascript onRpc("web_read", "res.partner", ({ args }) => { expect.step(args[0]); }); `onRpc`: for *every* ORM method/model ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When the *only* argument is a callback, it is expected to be an ORM callback to be called for *every* ORM call: .. code-block:: javascript onRpc(({ method }) => { expect.step(method); // Will step every ORM method call on every model }); Mock models: fields ------------------- Model fields can be declared in 2 ways: - as `public class fields `_; - under the `_fields` special key. For example: .. code-block:: javascript test("test view with date fields", async () => { // `_fields` can be assigned over, or extended directly. ResPartner._fields.date = fields.Date({ string: "Registration date" }); }); Field constructors can take a parameters dictionary to dictate their behaviour. It will be required for some of them, like relational fields, which need a `relation` property to work correctly. There are limits to what can be done with a mock field compared to an actual Python server field, but expect the most basic properties to be supported: `readonly`, `required`, `string`, etc. `compute` and `related` do work for the most basic use-cases, but do not expect them to function reliably as they would on the actual server. .. note:: There are 4 default fields pre-defined for each created model: `id`, `display_name`, `created_at` and `updated_at`. They match their server-side counterpart in their behaviour (e.g. `id` is incremental and `display_name` has a `compute` function similar to its server counterpart), and can be overridden if needed. Mock models: records -------------------- Model records are generated based on each object contained in the `_records` special key *when the model is loaded*. They are validated based on the fields available on the current models; if a property does not match a field defined on the model, an error is thrown. .. important:: `_records` *cannot* be altered *after* the model has been loaded, i.e. after the mock server has started. This key is only used to generate initial records. If records should be added *after* model creation, do it either form the available components in the UI, or through direct ORM calls on the mock server instance. Mock models: views ------------------ Since actual views need an `"ir.ui.view"` model to be declared, mock models use a simplified *mapping* to provide view arches. The `_view` special key is a dictionary, with its *keys* being view types, optionally accompanied by a view ID, and its *values* being the XML arch string representation. By default, view IDs are `false`, but can be specified explicitly with a comma-separated key combining the view type and its ID: .. code-block:: javascript // Will simulate a list view with no ID (false). ResPartner._views.list = /* xml */ ` `; // Will simulate a form view with ID 418. ResPartner._views["form,418"] = /* xml */ `
`; .. _mock-server/spawning: Spawning a mock server ====================== Just like in most cases, only one server can be active for a given test. As mentioned above, creating an `env` will automatically deploy a mock server. This means that all of these methods will *also* create a mock server, since they do create an `env`: - :ref:`makeMockEnv `; - :ref:`mountWithCleanup ` (calling :ref:`makeMockEnv `); - :ref:`mountView ` (calling :ref:`mountWithCleanup `). However, some low-level features may require to spawn a mock server *without* an environment. For that purpose, a `makeMockServer` helper can be called separately to initiate a mock server. .. note:: `makeMockServer` should *only* be used by low-level features, such as testing the `rpc` function without the environment. It is not meant to be used as a means to retrieve the current mock server instance. For that purpose, refer to :ref:`MockServer.current `. .. note:: It is to be noted that subsequent calls to `makeMockServer` after a mock server has been started are simply ignored. .. _mock-server/interacting: Interacting with the server =========================== While most of the server interactions are expected to be done directly or indirectly by production code spawned in the test case, it is sometimes meaningful to bypass the UI and call the mock server directly (e.g. to simulate that another user, somewhere else, somehow, has altered the database). This can be done by retrieving the `MockServer.current` static property containing the current mock server instance (only after initialization): .. code-block:: javascript // 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" }, ]); .. tip:: `MockServer.env` is just a shortcut to `MockServer.current.env`. .. _fetch: https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch .. _public-class-fields: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields .. _Request: https://developer.mozilla.org/en-US/docs/Web/API/Request .. _Response: https://developer.mozilla.org/en-US/docs/Web/API/Response .. _XMLHttpRequest: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest