|
6 | 6 |
|
7 | 7 | # testgres |
8 | 8 |
|
9 | | -PostgreSQL testing utility. Python 3.7.17+ is supported. |
10 | | - |
| 9 | +Utility for orchestrating temporary PostgreSQL clusters in Python tests. Supports Python 3.7.17 and newer. |
11 | 10 |
|
12 | 11 | ## Installation |
13 | 12 |
|
14 | | -To install `testgres`, run: |
| 13 | +Install `testgres` from PyPI: |
15 | 14 |
|
16 | | -``` |
| 15 | +```sh |
17 | 16 | pip install testgres |
18 | 17 | ``` |
19 | 18 |
|
20 | | -We encourage you to use `virtualenv` for your testing environment. |
21 | | - |
| 19 | +Use a dedicated virtual environment for isolated test dependencies. |
22 | 20 |
|
23 | 21 | ## Usage |
24 | 22 |
|
25 | 23 | ### Environment |
26 | 24 |
|
27 | | -> Note: by default testgres runs `initdb`, `pg_ctl`, `psql` provided by `PATH`. |
| 25 | +> Note: by default `testgres` invokes `initdb`, `pg_ctl`, and `psql` binaries found in `PATH`. |
28 | 26 |
|
29 | | -There are several ways to specify a custom postgres installation: |
| 27 | +Specify a custom PostgreSQL installation in one of the following ways: |
30 | 28 |
|
31 | | -* export `PG_CONFIG` environment variable pointing to the `pg_config` executable; |
32 | | -* export `PG_BIN` environment variable pointing to the directory with executable files. |
| 29 | +- Set the `PG_CONFIG` environment variable to point to the `pg_config` executable. |
| 30 | +- Set the `PG_BIN` environment variable to point to the directory with PostgreSQL binaries. |
33 | 31 |
|
34 | 32 | Example: |
35 | 33 |
|
36 | | -```bash |
37 | | -export PG_BIN=$HOME/pg_10/bin |
| 34 | +```sh |
| 35 | +export PG_BIN=$HOME/pg_16/bin |
38 | 36 | python my_tests.py |
39 | 37 | ``` |
40 | 38 |
|
41 | | - |
42 | 39 | ### Examples |
43 | 40 |
|
44 | | -Here is an example of what you can do with `testgres`: |
| 41 | +Create a temporary node, run queries, and let `testgres` clean up automatically: |
45 | 42 |
|
46 | 43 | ```python |
47 | | -# create a node with random name, port, etc |
| 44 | +# create a node with a random name, port, and data directory |
48 | 45 | with testgres.get_new_node() as node: |
49 | 46 |
|
50 | | - # run inidb |
| 47 | + # run initdb |
51 | 48 | node.init() |
52 | 49 |
|
53 | 50 | # start PostgreSQL |
54 | 51 | node.start() |
55 | 52 |
|
56 | | - # execute a query in a default DB |
| 53 | + # execute a query in the default database |
57 | 54 | print(node.execute('select 1')) |
58 | 55 |
|
59 | | -# ... node stops and its files are about to be removed |
| 56 | +# the node is stopped and its files are removed automatically |
60 | 57 | ``` |
61 | 58 |
|
62 | | -There are four API methods for running queries: |
| 59 | +### Query helpers |
| 60 | + |
| 61 | +`testgres` provides four helpers for executing queries against the node: |
63 | 62 |
|
64 | 63 | | Command | Description | |
65 | | -|----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| |
66 | | -| `node.psql(query, ...)` | Runs query via `psql` command and returns tuple `(error code, stdout, stderr)`. | |
67 | | -| `node.safe_psql(query, ...)` | Same as `psql()` except that it returns only `stdout`. If an error occurs during the execution, an exception will be thrown. | |
68 | | -| `node.execute(query, ...)` | Connects to PostgreSQL using `psycopg2` or `pg8000` (depends on which one is installed in your system) and returns two-dimensional array with data. | |
69 | | -| `node.connect(dbname, ...)` | Returns connection wrapper (`NodeConnection`) capable of running several queries within a single transaction. | |
| 64 | +|---------|-------------| |
| 65 | +| `node.psql(query, ...)` | Runs the query via `psql` and returns a tuple `(returncode, stdout, stderr)`. | |
| 66 | +| `node.safe_psql(query, ...)` | Same as `psql()` but returns only `stdout` and raises if the command fails. | |
| 67 | +| `node.execute(query, ...)` | Connects via `psycopg2` or `pg8000` (whichever is available) and returns a list of tuples. | |
| 68 | +| `node.connect(dbname, ...)` | Returns a `NodeConnection` wrapper for executing multiple statements within a transaction. | |
| 69 | + |
| 70 | +Example of transactional usage: |
70 | 71 |
|
71 | | -The last one is the most powerful: you can use `begin(isolation_level)`, `commit()` and `rollback()`: |
72 | 72 | ```python |
73 | 73 | with node.connect() as con: |
74 | 74 | con.begin('serializable') |
75 | 75 | print(con.execute('select %s', 1)) |
76 | 76 | con.rollback() |
77 | 77 | ``` |
78 | 78 |
|
79 | | - |
80 | 79 | ### Logging |
81 | 80 |
|
82 | | -By default, `cleanup()` removes all temporary files (DB files, logs etc) that were created by testgres' API methods. |
83 | | -If you'd like to keep logs, execute `configure_testgres(node_cleanup_full=False)` before running any tests. |
| 81 | +By default `cleanup()` removes all temporary files (data directories, logs, and so on) created by the API. Call `configure_testgres(node_cleanup_full=False)` before starting nodes if you want to keep logs for inspection. |
84 | 82 |
|
85 | | -> Note: context managers (aka `with`) call `stop()` and `cleanup()` automatically. |
| 83 | +> Note: context managers (the `with` statement) call `stop()` and `cleanup()` automatically. |
86 | 84 |
|
87 | | -`testgres` supports [python logging](https://docs.python.org/3.6/library/logging.html), |
88 | | -which means that you can aggregate logs from several nodes into one file: |
| 85 | +`testgres` integrates with the standard [Python logging](https://docs.python.org/3/library/logging.html) module, so you can aggregate logs from multiple nodes: |
89 | 86 |
|
90 | 87 | ```python |
91 | 88 | import logging |
92 | 89 |
|
93 | 90 | # write everything to /tmp/testgres.log |
94 | 91 | logging.basicConfig(filename='/tmp/testgres.log') |
95 | 92 |
|
96 | | -# enable logging, and create two different nodes |
| 93 | +# enable logging and create two nodes |
97 | 94 | testgres.configure_testgres(use_python_logging=True) |
98 | 95 | node1 = testgres.get_new_node().init().start() |
99 | 96 | node2 = testgres.get_new_node().init().start() |
100 | 97 |
|
101 | | -# execute a few queries |
102 | 98 | node1.execute('select 1') |
103 | 99 | node2.execute('select 2') |
104 | 100 |
|
105 | 101 | # disable logging |
106 | 102 | testgres.configure_testgres(use_python_logging=False) |
107 | 103 | ``` |
108 | 104 |
|
109 | | -Look at `tests/test_simple.py` file for a complete example of the logging |
110 | | -configuration. |
111 | | - |
| 105 | +See `tests/test_simple.py` for a complete logging example. |
112 | 106 |
|
113 | | -### Backup & replication |
| 107 | +### Backup and replication |
114 | 108 |
|
115 | | -It's quite easy to create a backup and start a new replica: |
| 109 | +Creating backups and spawning replicas is straightforward: |
116 | 110 |
|
117 | 111 | ```python |
118 | 112 | with testgres.get_new_node('master') as master: |
119 | 113 | master.init().start() |
120 | 114 |
|
121 | | - # create a backup |
122 | 115 | with master.backup() as backup: |
123 | | - |
124 | | - # create and start a new replica |
125 | 116 | replica = backup.spawn_replica('replica').start() |
126 | | - |
127 | | - # catch up with master node |
128 | 117 | replica.catchup() |
129 | 118 |
|
130 | | - # execute a dummy query |
131 | 119 | print(replica.execute('postgres', 'select 1')) |
132 | 120 | ``` |
133 | 121 |
|
134 | 122 | ### Benchmarks |
135 | 123 |
|
136 | | -`testgres` is also capable of running benchmarks using `pgbench`: |
| 124 | +Use `pgbench` through `testgres` to run quick benchmarks: |
137 | 125 |
|
138 | 126 | ```python |
139 | 127 | with testgres.get_new_node('master') as master: |
140 | | - # start a new node |
141 | 128 | master.init().start() |
142 | 129 |
|
143 | | - # initialize default DB and run bench for 10 seconds |
144 | | - res = master.pgbench_init(scale=2).pgbench_run(time=10) |
145 | | - print(res) |
| 130 | + result = master.pgbench_init(scale=2).pgbench_run(time=10) |
| 131 | + print(result) |
146 | 132 | ``` |
147 | 133 |
|
148 | | - |
149 | 134 | ### Custom configuration |
150 | 135 |
|
151 | | -It's often useful to extend default configuration provided by `testgres`. |
152 | | - |
153 | | -`testgres` has `default_conf()` function that helps control some basic |
154 | | -options. The `append_conf()` function can be used to add custom |
155 | | -lines to configuration lines: |
| 136 | +`testgres` ships with sensible defaults. Adjust them as needed with `default_conf()` and `append_conf()`: |
156 | 137 |
|
157 | 138 | ```python |
158 | | -ext_conf = "shared_preload_libraries = 'postgres_fdw'" |
| 139 | +extra_conf = "shared_preload_libraries = 'postgres_fdw'" |
159 | 140 |
|
160 | | -# initialize a new node |
161 | 141 | with testgres.get_new_node().init() as master: |
162 | | - |
163 | | - # ... do something ... |
164 | | - |
165 | | - # reset main config file |
166 | | - master.default_conf(fsync=True, |
167 | | - allow_streaming=True) |
168 | | - |
169 | | - # add a new config line |
170 | | - master.append_conf('postgresql.conf', ext_conf) |
| 142 | + master.default_conf(fsync=True, allow_streaming=True) |
| 143 | + master.append_conf('postgresql.conf', extra_conf) |
171 | 144 | ``` |
172 | 145 |
|
173 | | -Note that `default_conf()` is called by `init()` function; both of them overwrite |
174 | | -the configuration file, which means that they should be called before `append_conf()`. |
| 146 | +`default_conf()` is called by `init()` and rewrites the configuration file. Apply `append_conf()` afterwards to keep custom lines. |
175 | 147 |
|
176 | 148 | ### Remote mode |
177 | | -Testgres supports the creation of PostgreSQL nodes on a remote host. This is useful when you want to run distributed tests involving multiple nodes spread across different machines. |
178 | 149 |
|
179 | | -To use this feature, you need to use the RemoteOperations class. This feature is only supported with Linux. |
180 | | -Here is an example of how you might set this up: |
| 150 | +You can provision nodes on a remote host (Linux only) by wiring `RemoteOperations` into the configuration: |
181 | 151 |
|
182 | 152 | ```python |
183 | 153 | from testgres import ConnectionParams, RemoteOperations, TestgresConfig, get_remote_node |
184 | 154 |
|
185 | | -# Set up connection params |
186 | 155 | conn_params = ConnectionParams( |
187 | | - host='your_host', # replace with your host |
188 | | - username='user_name', # replace with your username |
189 | | - ssh_key='path_to_ssh_key' # replace with your SSH key path |
| 156 | + host='example.com', |
| 157 | + username='postgres', |
| 158 | + ssh_key='/path/to/ssh/key' |
190 | 159 | ) |
191 | 160 | os_ops = RemoteOperations(conn_params) |
192 | 161 |
|
193 | | -# Add remote testgres config before test |
194 | 162 | TestgresConfig.set_os_ops(os_ops=os_ops) |
195 | 163 |
|
196 | | -# Proceed with your test |
197 | | -def test_basic_query(self): |
| 164 | +def test_basic_query(): |
198 | 165 | with get_remote_node(conn_params=conn_params) as node: |
199 | 166 | node.init().start() |
200 | | - res = node.execute('SELECT 1') |
201 | | - self.assertEqual(res, [(1,)]) |
| 167 | + assert node.execute('SELECT 1') == [(1,)] |
202 | 168 | ``` |
203 | 169 |
|
| 170 | +### Pytest integration |
| 171 | + |
| 172 | +Use fixtures to create and clean up nodes automatically when testing with `pytest`: |
| 173 | + |
| 174 | +```python |
| 175 | +import pytest |
| 176 | +import testgres |
| 177 | + |
| 178 | +@pytest.fixture |
| 179 | +def pg_node(): |
| 180 | + node = testgres.get_new_node().init().start() |
| 181 | + try: |
| 182 | + yield node |
| 183 | + finally: |
| 184 | + node.stop() |
| 185 | + node.cleanup() |
| 186 | + |
| 187 | +def test_simple(pg_node): |
| 188 | + assert pg_node.execute('select 1')[0][0] == 1 |
| 189 | +``` |
| 190 | + |
| 191 | +This pattern keeps tests concise and ensures that every node is stopped and removed even if the test fails. |
| 192 | + |
| 193 | +### Scaling tips |
| 194 | + |
| 195 | +- Run tests in parallel with `pytest -n auto` (requires `pytest-xdist`). Ensure each node uses a distinct port by setting `PGPORT` in the fixture or by passing the `port` argument to `get_new_node()`. |
| 196 | +- Always call `node.cleanup()` after each test, or rely on context managers/fixtures that do it for you, to avoid leftover data directories. |
| 197 | +- Prefer `node.safe_psql()` for lightweight assertions that should fail fast; use `node.execute()` when you need structured Python results. |
| 198 | + |
204 | 199 | ## Authors |
205 | 200 |
|
206 | 201 | [Ildar Musin](https://github.com/zilder) |
207 | 202 | [Dmitry Ivanov](https://github.com/funbringer) |
208 | 203 | [Ildus Kurbangaliev](https://github.com/ildus) |
209 | | -[Yury Zhuravlev](https://github.com/stalkerg) |
| 204 | +[Yury Zhuravlev](https://github.com/stalkerg) |
0 commit comments