|
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 variablepointingto the`pg_config` executable; |
32 | | -* export`PG_BIN` environment variablepointingto the directory withexecutable files. |
| 29 | +- Set the`PG_CONFIG` environment variableto pointto the`pg_config` executable. |
| 30 | +- Set the`PG_BIN` environment variableto pointto the directory withPostgreSQL 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 witharandom name, port,and data directory |
48 | 45 | with testgres.get_new_node()as node: |
49 | 46 |
|
50 | | -# runinidb |
| 47 | +# runinitdb |
51 | 48 | node.init() |
52 | 49 |
|
53 | 50 | # start PostgreSQL |
54 | 51 | node.start() |
55 | 52 |
|
56 | | -# execute a query ina defaultDB |
| 53 | +# execute a query inthe defaultdatabase |
57 | 54 | print(node.execute('select 1')) |
58 | 55 |
|
59 | | -#... nodestopsand its files areabout to beremoved |
| 56 | +#the nodeis stoppedand 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 | +###Backupand replication |
114 | 108 |
|
115 | | -It's quite easy to create a backupandstart a new replica: |
| 109 | +Creating backupsandspawning 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 testgresimport 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 keypath |
| 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 | | -deftest_basic_query(self): |
| 164 | +deftest_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 | +defpg_node(): |
| 180 | + node= testgres.get_new_node().init().start() |
| 181 | +try: |
| 182 | +yield node |
| 183 | +finally: |
| 184 | + node.stop() |
| 185 | + node.cleanup() |
| 186 | + |
| 187 | +deftest_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) |