Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 128 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# RavenDB Test Driver

`ravendb-test-driver` runs your integration tests against a **real** RavenDB server instead of a
mock. Each test gets its own isolated database, created on demand and torn down afterwards, so
tests do not leak state into one another. You write ordinary `ravendb` client code; the driver
handles the server and the per-test database lifecycle.
`ravendb-test-driver` runs integration tests against a real RavenDB server instead of a mock. It
creates an isolated database for each test and deletes that database when its `DocumentStore` is
closed. Your tests use the standard `ravendb` client API.

## Install

Expand All @@ -13,29 +12,55 @@ pip install ravendb-test-driver

Python 3.10+ is required.

## Providing a server: pick one
## Quick start

The driver needs a RavenDB server to talk to. Choose how it should get one based on your
environment.
With the default configuration, the driver starts an embedded RavenDB server and gives every
store its own database:

### 1. Embedded server (default, needs .NET)
```python
from ravendb_test_driver import RavenTestDriver

with RavenTestDriver() as driver:
with driver.get_document_store() as store:
with store.open_session() as session:
session.store({"name": "John"}, "people/1")
session.save_changes()

with store.open_session() as session:
assert session.load("people/1", dict)["name"] == "John"
```

The default mode requires a matching system .NET runtime. The two alternatives below do not
require .NET on the machine running the Python tests.

## Choose where RavenDB runs

| Mode | .NET on the test machine? | Who manages the server? | Best for |
|------|---------------------------|-------------------------|----------|
| [Embedded default](#embedded-server-default) | Yes | Test driver | The simplest local setup |
| [On-demand self-contained](#on-demand-self-contained-server) | No | Test driver | Portable developer machines and CI runners |
| [Attach to your server](#attach-to-a-server-you-run) | No | You | Docker, Testcontainers, or a shared service |

Out of the box the driver boots an **embedded** RavenDB server (via `ravendb-embedded`). Nothing
to configure, but the embedded server is a .NET application, so a matching runtime must be
installed:
Configure the selected mode before the first call to `get_document_store()`.

### Embedded server (default)

No configuration is needed. The driver starts the framework-dependent server bundled with
`ravendb-embedded`.

| `ravendb-test-driver` version | Required runtime |
|-------------------------------|------------------|
| 7.2.x | .NET 10 |
| 7.1.x | .NET 8 |

Check with `dotnet --list-runtimes`. The requirement tracks the embedded server and can change on
a minor upgrade, so re-check it when you bump versions.
Run `dotnet --list-runtimes` and look for `Microsoft.NETCore.App`. Re-check the requirement when
upgrading to a new RavenDB minor version.

### 2. Self-contained embedded server (no system .NET)
Runnable walkthrough: [Lab 02 — isolated embedded databases](labs/02-embedded-per-test.md).

Let the driver download and cache a self-contained RavenDB build. It manages the server for you
without calling `dotnet`:
### On-demand self-contained server

Let the driver download, cache, and manage the self-contained build for the current platform:

```python
from ravendb_embedded import ServerOptions
Expand All @@ -44,83 +69,128 @@ from ravendb_test_driver import RavenTestDriver
options = ServerOptions()
options.with_auto_downloaded_server()
RavenTestDriver.configure_server(options)

with RavenTestDriver() as driver:
with driver.get_document_store() as store:
...
```

The embedded package detects the host operating system and architecture, so the same test
configuration is portable across supported Windows, Linux, and macOS machines.
The same test configuration works across supported Windows, Linux, and macOS machines because the
operating system and architecture are detected at runtime. The first run downloads 100 MB+;
later runs reuse `~/.cache/ravendb-embedded`. Pass `cache_root` to
`with_auto_downloaded_server()` when your build system restores a different cache directory.

Call `configure_server()` before the first `get_document_store()`. The first run downloads
100 MB+; later runs reuse the cache. Self-contained mode needs no system .NET, but normal RavenDB
OS dependencies still apply; minimal Linux images may need their distribution's ICU package. See
[`labs/04-embedded-no-dotnet.md`](labs/04-embedded-no-dotnet.md).
Supported targets are Windows x64/x86, Linux x64/ARM64, and macOS x64/ARM64. Self-contained mode
removes the system .NET requirement, but normal RavenDB operating-system dependencies still
apply. Minimal Linux images may need their distribution's ICU package. The Python wheel stays
platform-independent because it downloads only the self-contained build needed by the current
machine rather than bundling every platform.

### 3. Attach to a server you run yourself (no .NET)
Runnable walkthrough: [Lab 04 — portable embedded tests without .NET](labs/04-embedded-no-dotnet.md).

If you would rather not put .NET on the test machine (containerized CI, locked-down hosts), run
RavenDB yourself (Docker, testcontainers, a shared CI service) and point the driver at its URL.
The driver skips the embedded boot entirely and still creates an isolated database per test.
### Attach to a server you run

Start RavenDB yourself—locally, in Docker or Testcontainers, or as a shared service—and configure
its URL:

```python
from ravendb_test_driver import RavenTestDriver

RavenTestDriver.configure_external_server("http://localhost:8080")
# or set RAVENDB_TEST_SERVER_URL in the environment
```

Alternatively, configure the URL through the environment:

```bash
RAVENDB_TEST_SERVER_URL=http://localhost:8080 python -m unittest
```

This path does not use `EmbeddedServer`: the driver neither starts nor stops the server, but it
still creates and deletes an isolated database for each test. No .NET installation is needed on
the test machine; the server environment supplies its own runtime.

For HTTPS with client-certificate authentication:

```python
RavenTestDriver.configure_external_server(
"https://my-ravendb",
certificate_pem_path="client.pem",
trust_store_path="ca.crt", # needed when the server CA is not already trusted
trust_store_path="ca.crt",
)
```

The equivalent environment variables are `RAVENDB_TEST_SERVER_URL`,
`RAVENDB_TEST_SERVER_CERT`, and `RAVENDB_TEST_SERVER_CA`. Call the configuration method once,
before the first `get_document_store()`. A runnable Docker / testcontainers guide is in
[`labs/01-attach-to-server.md`](labs/01-attach-to-server.md).
The equivalent environment variables are:

- `RAVENDB_TEST_SERVER_URL`
- `RAVENDB_TEST_SERVER_CERT`
- `RAVENDB_TEST_SERVER_CA`

## Usage
`trust_store_path` or `RAVENDB_TEST_SERVER_CA` is needed when the server's CA is not already
trusted by the test machine.

Subclass `RavenTestDriver` (or hold an instance) and call `get_document_store()` in each test to
get a store backed by a fresh database:
Runnable walkthrough: [Lab 01 — Docker, Testcontainers, and shared servers](labs/01-attach-to-server.md).

## Test lifecycle

Create a `RavenTestDriver` for the test or fixture, then close every returned store. A context
manager handles both steps:

```python
from unittest import TestCase
from ravendb_test_driver import RavenTestDriver


class TestBasic(TestCase):
def setUp(self):
self.test_driver = RavenTestDriver()
class TestPeople(TestCase):
def test_stores_a_person(self):
with RavenTestDriver() as driver:
with driver.get_document_store() as store:
with store.open_session() as session:
session.store({"name": "John"}, "people/1")
session.save_changes()
```

Each `get_document_store()` call creates a new database. Closing the store deletes it, which keeps
tests independent even when they share one RavenDB server process.

def test_stores_a_document(self):
with self.test_driver.get_document_store() as store: # isolated database
with store.open_session() as session:
session.store({"Name": "John"}, "people/1")
session.save_changes()
## Seed data and wait for indexing

Override `setup_database(self, store)` to create indexes or seed reference data whenever the
driver creates a database:

```python
class PeopleTestDriver(RavenTestDriver):
def setup_database(self, store):
with store.open_session() as session:
session.store({"name": "Seeded"}, "people/seed")
session.save_changes()
```

Runnable example: [`labs/02-embedded-per-test.md`](labs/02-embedded-per-test.md).
Use `GetDocumentStoreOptions.wait_for_indexing_timeout` when a store should not be returned until
indexing settles, or call `wait_for_indexing(store)` directly.

### Seeding data and waiting for indexes
`wait_for_user_to_continue_the_test(store)` opens RavenDB Studio and pauses the test for manual
inspection.

- Override `setup_database(self, store)` to seed or configure every database the driver hands
out (indexes, reference data, and so on).
- `get_document_store(options)` accepts `GetDocumentStoreOptions`; set a
`wait_for_indexing_timeout` to block until indexing settles, or call
`wait_for_indexing(store)` yourself.
- `wait_for_user_to_continue_the_test(store)` opens RavenDB Studio so you can inspect the data
mid-test.
Runnable walkthrough: [Lab 03 — seeding and indexes](labs/03-seeding-indexes.md).

Runnable example: [`labs/03-seeding-indexes.md`](labs/03-seeding-indexes.md).
## Labs

## Links
| Lab | Scenario | Needs system .NET? |
|-----|----------|--------------------|
| [01](labs/01-attach-to-server.md) | Attach to Docker, Testcontainers, or a shared server | No |
| [02](labs/02-embedded-per-test.md) | Default embedded server and isolated databases | Yes |
| [03](labs/03-seeding-indexes.md) | Seed data and wait for real indexing | Yes |
| [04](labs/04-embedded-no-dotnet.md) | On-demand self-contained server | No |

The runnable lab scripts live in this repository and are not installed into `site-packages`.
The runnable scripts live in this repository rather than `site-packages`. Clone or download the
repository, install the package, and run them from the repository root. See the
[complete labs guide](labs/README.md).

For lower-level server configuration, see
[`ravendb-embedded`](https://github.com/ravendb/ravendb-python-embedded).

## Links

- PyPI: https://pypi.org/project/ravendb-test-driver/
- GitHub: https://github.com/ravendb/ravendb-python-testdriver
- Server and self-contained options: https://github.com/ravendb/ravendb-python-embedded
- [PyPI](https://pypi.org/project/ravendb-test-driver/)
- [Source](https://github.com/ravendb/ravendb-python-testdriver)
- [RavenDB Python client documentation](https://ravendb.net/docs/article-page/latest/python)
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
setup(
name="ravendb-test-driver",
packages=find_packages(exclude=["*.tests.*", "tests", "*.tests", "tests.*"]),
version="7.2.5.post1",
version="7.2.5.post2",
description="RavenDB package for writing integration tests against RavenDB server",
long_description_content_type="text/markdown",
long_description=open("README.md").read(),
Expand All @@ -14,5 +14,5 @@
keywords=["ravendb", "nosql", "database", "test", "driver"],
python_requires=">=3.10",
license_files=["LICENSE"],
install_requires=["ravendb-embedded==7.2.5.post1", "ravendb==7.2.3.post1"],
install_requires=["ravendb-embedded==7.2.5.post2", "ravendb==7.2.3.post1"],
)
14 changes: 14 additions & 0 deletions tests/test_a_secured_attach.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
from ravendb import Lazy
from ravendb.exceptions.raven_exceptions import RavenException
from ravendb_embedded import EmbeddedServer, ServerOptions

from ravendb_test_driver import RavenTestDriver
Expand Down Expand Up @@ -140,3 +141,16 @@ def test_api_and_environment_credentials_reach_database_store(self):
for name, value in original_environment.items():
if value is not None:
os.environ[name] = value

def test_https_attach_requires_a_client_certificate(self):
original_environment = {name: os.environ.get(name) for name in self._ENV_NAMES}
self._reset_driver()
try:
RavenTestDriver.configure_external_server("https://127.0.0.1:1")
with self.assertRaisesRegex(RavenException, "needs a client certificate"):
RavenTestDriver.run_server()
finally:
self._reset_driver()
for name, value in original_environment.items():
if value is not None:
os.environ[name] = value
Loading