mirror of
https://github.com/meilisearch/meilisearch.git
synced 2025-09-23 21:26:26 +00:00
266 lines
6.6 KiB
Markdown
266 lines
6.6 KiB
Markdown
# Declarative upgrade tests
|
||
|
||
Declarative upgrade tests ensure that Meilisearch features remain stable across versions.
|
||
|
||
While we already have unit tests, those are run against **temporary databases** that are created fresh each time and therefore never risk corruption.
|
||
|
||
Upgrade tests instead **simulate the lifetime of a database**: they chain together commands and version upgrades, verifying that database state and API responses remain consistent.
|
||
|
||
## Basic example
|
||
|
||
```json
|
||
{
|
||
"type": "test",
|
||
"name": "api-keys",
|
||
"initialVersion": "1.19.0", // the first command will run on a brand new database of this version
|
||
"commands": []
|
||
}
|
||
```
|
||
|
||
This example defines a no-op test (it does nothing).
|
||
|
||
If the file is saved at `workloads/tests/example.json`, you can run it with:
|
||
|
||
```bash
|
||
cargo xtask test workloads/tests/example.json
|
||
```
|
||
|
||
## Commands
|
||
|
||
Commands represent API requests sent to Meilisearch endpoints during a test.
|
||
|
||
They are executed sequentially, and their responses can be validated to ensure consistent behavior across upgrades.
|
||
|
||
```json
|
||
|
||
{
|
||
"route": "keys",
|
||
"method": "POST",
|
||
"body": {
|
||
"inline": {
|
||
"actions": [
|
||
"search",
|
||
"documents.add"
|
||
],
|
||
"description": "Test API Key",
|
||
"expiresAt": null,
|
||
"indexes": [ "movies" ]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
This command issues a `POST /keys` request, creating an API key with permissions to search and add documents in the `movies` index.
|
||
|
||
### Using assets in commands
|
||
|
||
To keep tests concise and reusable, you can define **assets** at the root of the workload file.
|
||
|
||
Assets are external data sources (such as datasets) that are cached between runs, making tests faster and easier to read.
|
||
|
||
```json
|
||
{
|
||
"type": "test",
|
||
"name": "movies",
|
||
"initialVersion": "1.12.0",
|
||
"assets": {
|
||
"movies.json": {
|
||
"local_location": null,
|
||
"remote_location": "https://milli-benchmarks.fra1.digitaloceanspaces.com/bench/datasets/movies.json",
|
||
"sha256": "5b6e4cb660bc20327776e8a33ea197b43d9ec84856710ead1cc87ab24df77de1"
|
||
}
|
||
},
|
||
"commands": [
|
||
{
|
||
"route": "indexes/movies/documents",
|
||
"method": "POST",
|
||
"body": {
|
||
"asset": "movies.json"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
In this example:
|
||
- The `movies.json` dataset is defined as an asset, pointing to a remote URL.
|
||
- The SHA-256 checksum ensures integrity.
|
||
- The `POST /indexes/movies/documents` command uses this asset as the request body.
|
||
|
||
This makes the test much cleaner than inlining a large dataset directly into the command.
|
||
|
||
### Asserting responses
|
||
|
||
Commands can specify both the **expected status code** and the **expected response body**.
|
||
|
||
```json
|
||
{
|
||
"route": "indexes/movies/documents",
|
||
"method": "POST",
|
||
"body": {
|
||
"asset": "movies.json"
|
||
},
|
||
"expectedStatus": 202,
|
||
"expectedResponse": {
|
||
"enqueuedAt": "[timestamp]", // Set to a bracketed string to ignore the value
|
||
"indexUid": "movies",
|
||
"status": "enqueued",
|
||
"taskUid": 1,
|
||
"type": "documentAdditionOrUpdate"
|
||
},
|
||
"synchronous": "WaitForTask"
|
||
}
|
||
```
|
||
|
||
Manually writing `expectedResponse` fields can be tedious.
|
||
|
||
Instead, you can let the test runner populate them automatically:
|
||
|
||
```bash
|
||
# Run the workload to populate expected fields. Only adds the missing ones, doesn't change existing data
|
||
cargo xtask test workloads/tests/example.json --add-missing-responses
|
||
|
||
# OR
|
||
|
||
# Run the workload to populate expected fields. Updates all fields including existing ones
|
||
cargo xtask test workloads/tests/example.json --update-responses
|
||
```
|
||
|
||
This workflow is recommended:
|
||
|
||
1. Write the test without expected fields.
|
||
2. Run it with `--add-missing-responses` to capture the actual responses.
|
||
3. Review and commit the generated expectations.
|
||
|
||
## Upgrade commands
|
||
|
||
Upgrade commands allow you to switch the Meilisearch instance from one version to another during a test.
|
||
|
||
When executed, an upgrade command will:
|
||
1. Stop the current Meilisearch server.
|
||
2. Upgrade the database to the specified version.
|
||
3. Restart the server with the new specified version.
|
||
|
||
### Typical Usage
|
||
|
||
In most cases, you will:
|
||
|
||
- **Set up** some data using commands on an older version.
|
||
- **Upgrade** to the latest version.
|
||
- **Assert** that the data and API behavior remain correct after the upgrade.
|
||
|
||
```json
|
||
{
|
||
"type": "test",
|
||
"name": "movies",
|
||
"initialVersion": "1.12.0", // An older version to start with
|
||
"commands": [
|
||
// Commands to populate the database
|
||
{
|
||
"upgrade": "latest" // Will build meilisearch locally and run it
|
||
},
|
||
// Commands to check the state of the database
|
||
]
|
||
}
|
||
```
|
||
|
||
This ensures backward compatibility: databases created with older Meilisearch versions should remain functional and consistent after an upgrade.
|
||
|
||
### Advanced usage
|
||
|
||
As time goes on, tests may grow more complex as they evolve alongside new features and schema changes.
|
||
A single test can chain together multiple upgrades, interleaving data population, API checks, and version transitions.
|
||
|
||
For example:
|
||
|
||
```json
|
||
{
|
||
"type": "test",
|
||
"name": "movies",
|
||
"initialVersion": "1.12.0",
|
||
"commands": [
|
||
// Commands to populate the database
|
||
{
|
||
"upgrade": "1.17.0"
|
||
},
|
||
// Commands on endpoints that were removed after 1.17
|
||
{
|
||
"upgrade": "latest"
|
||
},
|
||
// Check the state
|
||
]
|
||
}
|
||
```
|
||
|
||
## Variables
|
||
|
||
Sometimes a command needs to use a value returned by a **previous response**.
|
||
These values can be captured and reused using the register field.
|
||
|
||
```json
|
||
{
|
||
"route": "keys",
|
||
"method": "POST",
|
||
"body": {
|
||
"inline": {
|
||
"actions": [
|
||
"search",
|
||
"documents.add"
|
||
],
|
||
"description": "Test API Key",
|
||
"expiresAt": null,
|
||
"indexes": [ "movies" ]
|
||
}
|
||
},
|
||
"expectedResponse": {
|
||
"key": "c6f64630bad2996b1f675007c8800168e14adf5d6a7bb1a400a6d2b158050eaf",
|
||
// ...
|
||
},
|
||
"register": {
|
||
"key": "/key"
|
||
},
|
||
"synchronous": "WaitForResponse"
|
||
}
|
||
```
|
||
|
||
The `register` field captures the value at the JSON path `/key` from the response.
|
||
Paths follow the **JavaScript Object Notation Pointer (RFC 6901)** format.
|
||
Registered variables are available for all subsequent commands.
|
||
|
||
Registered variables can be referenced by wrapping their name in double curly braces:
|
||
|
||
In the route/path:
|
||
|
||
```json
|
||
{
|
||
"route": "tasks/{{ task_id }}",
|
||
"method": "GET"
|
||
}
|
||
```
|
||
|
||
In the request body:
|
||
|
||
```json
|
||
{
|
||
"route": "indexes/movies/documents",
|
||
"method": "PATCH",
|
||
"body": {
|
||
"inline": {
|
||
"id": "{{ document_id }}",
|
||
"overview": "Shazam turns evil and the world is in danger.",
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
As an API-key:
|
||
|
||
```json
|
||
{
|
||
"route": "indexes/movies/documents",
|
||
"method": "POST",
|
||
"body": { /* ... */ },
|
||
"apiKeyVariable": "key" // The content of the key variable will be used as an API key
|
||
}
|
||
```
|