-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0567e58
commit 09b033e
Showing
10 changed files
with
370 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
name: Build | ||
|
||
on: | ||
push: | ||
branches: | ||
- '**' | ||
jobs: | ||
build: | ||
name: Build | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v4 | ||
|
||
- name: Setup Python | ||
uses: actions/setup-python@v4 | ||
with: | ||
python-version: '3.12' | ||
cache: 'pip' | ||
|
||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install -r requirements.txt | ||
- name: Test with pytest | ||
run: pytest |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
.idea | ||
venv/ | ||
.pytest_cache/ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
3.12 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,39 @@ | ||
# tc-guide-getting-started-with-testcontainers-for-python | ||
Getting started with Testcontainers for Java guide | ||
# Getting started with Testcontainers for Python | ||
|
||
This is sample code for [Getting started with Testcontainers for Python](https://testcontainers.com/guides/getting-started-with-testcontainers-for-python) guide. | ||
|
||
## 1. Setup Environment | ||
|
||
* Make sure you have a [compatible Docker environment](https://www.testcontainers.org/supported_docker_environment/) installed. | ||
* Python 3.12+ installed. | ||
|
||
For example: | ||
|
||
```shell | ||
$ python --version | ||
Python 3.12.0 | ||
``` | ||
|
||
## 2. Setup Project | ||
|
||
* Clone the repository | ||
|
||
```shell | ||
git clone https://github.com/testcontainers/tc-guide-getting-started-with-testcontainers-for-python.git | ||
cd tc-guide-getting-started-with-testcontainers-for-python | ||
python3 -m venv venv | ||
source venv/bin/activate | ||
pip install -r requirements.txt | ||
``` | ||
|
||
* Open the **tc-guide-getting-started-with-testcontainers-for-python** project in your favorite IDE. | ||
|
||
## 3. Run Tests | ||
|
||
Run the command to run the tests. | ||
|
||
```shell | ||
$ pytest | ||
``` | ||
|
||
The tests should pass. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
from db.connection import get_connection | ||
|
||
|
||
class Customer: | ||
def __init__(self, cust_id, name, email): | ||
self.id = cust_id | ||
self.name = name | ||
self.email = email | ||
|
||
def __str__(self): | ||
return f"Customer({self.id}, {self.name}, {self.email})" | ||
|
||
|
||
def create_table(): | ||
with get_connection() as conn: | ||
with conn.cursor() as cur: | ||
cur.execute(""" | ||
CREATE TABLE customers ( | ||
id serial PRIMARY KEY, | ||
name varchar not null, | ||
email varchar not null unique) | ||
""") | ||
conn.commit() | ||
|
||
|
||
def create_customer(c: Customer): | ||
with get_connection() as conn: | ||
with conn.cursor() as cur: | ||
cur.execute( | ||
"INSERT INTO customers (name, email) VALUES (%s, %s)", (c.name, c.email)) | ||
conn.commit() | ||
|
||
|
||
def get_all_customers() -> list[Customer]: | ||
with get_connection() as conn: | ||
with conn.cursor() as cur: | ||
cur.execute("SELECT * FROM customers") | ||
return [Customer(cid, name, email) for cid, name, email in cur] | ||
|
||
|
||
def get_customer_by_email(email) -> Customer: | ||
with get_connection() as conn: | ||
with conn.cursor() as cur: | ||
cur.execute("SELECT id, name, email FROM customers WHERE email = %s", (email,)) | ||
(cid, name, email) = cur.fetchone() | ||
return Customer(cid, name, email) | ||
|
||
|
||
def delete_all_customers(): | ||
with get_connection() as conn: | ||
with conn.cursor() as cur: | ||
cur.execute("DELETE FROM customers") | ||
conn.commit() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import os | ||
|
||
import psycopg | ||
|
||
|
||
def get_connection(): | ||
host = os.getenv("DB_HOST", "localhost") | ||
port = os.getenv("DB_PORT", "5432") | ||
username = os.getenv("DB_USERNAME", "postgres") | ||
password = os.getenv("DB_PASSWORD", "postgres") | ||
database = os.getenv("DB_NAME", "postgres") | ||
return psycopg.connect(f"host={host} dbname={database} user={username} password={password} port={port}") |
177 changes: 177 additions & 0 deletions
177
guide/getting-started-with-testcontainers-for-python/index.adoc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
--- | ||
title: "Getting started with Testcontainers for Python" | ||
date: 2023-12-04T09:39:58+05:30 | ||
draft: false | ||
description: This guide will help you to get started with Testcontainers for Python by demonstrating how you can use PostgreSQL for testing. | ||
repo: https://github.com/testcontainers/tc-guide-getting-started-with-testcontainers-for-python | ||
languages: | ||
- Python | ||
tags: | ||
- postgresql | ||
--- | ||
:toc: | ||
:toclevels: 2 | ||
:codebase: https://raw.githubusercontent.com/testcontainers/tc-guide-getting-started-with-testcontainers-for-python/main | ||
|
||
Testcontainers is an open-source framework for provisioning throwaway, on-demand containers for development and testing use cases. | ||
Testcontainers makes it easy to work with databases, message brokers, web browsers, or just about anything | ||
that can run in a Docker container. | ||
|
||
Using Testcontainers, you can write tests talking to the same type of services you use in production | ||
without mocks or in-memory services. | ||
|
||
[NOTE] | ||
If you are new to Testcontainers then please | ||
read https://testcontainers.com/guides/introducing-testcontainers[What is Testcontainers, and why should you use it?] | ||
to learn more about Testcontainers. | ||
|
||
Let us create a simple Python application that uses PostgreSQL database to store customers information. | ||
Then we will learn how to use Testcontainers for testing with a real Postgres database. | ||
|
||
== Create a Python application | ||
|
||
Let's create a Python project and use the venv module to create a virtual environment for our project. | ||
By using a virtual environment, we can avoid installing dependencies globally, | ||
and also we can use different versions of the same package in different projects. | ||
|
||
[source,shell] | ||
---- | ||
mkdir tc-python-demo | ||
cd tc-python-demo | ||
python3 -m venv venv | ||
source venv/bin/activate | ||
---- | ||
|
||
We are going to use https://www.psycopg.org/psycopg3/[psycopg3] for talking to the Postgres database, | ||
https://pytest.org/[pytest] for testing, | ||
and https://testcontainers-python.readthedocs.io/en/latest/README.html[testcontainers-python] for running a PostgreSQL database in a container. | ||
|
||
Once the virtual environment is activated, we can install the required dependencies using pip as follows: | ||
|
||
[source,shell] | ||
---- | ||
$ pip install psycopg pytest testcontainers-postgres | ||
$ pip freeze > requirements.txt | ||
---- | ||
|
||
Once the dependencies are installed, we have used *pip freeze* command to generate the *requirements.txt* file | ||
so that others can install the same versions of packages simply using *pip install -r requirements.txt*. | ||
|
||
== Implement Database Helper | ||
Let's create *db/connection.py* file and create a function to get database connection as follows: | ||
|
||
[source,python] | ||
---- | ||
include::{codebase}/db/connection.py[] | ||
---- | ||
|
||
Instead of hard-coding the database connection parameters, we are using environment variables to get the database connection parameters. | ||
This will help us to run the application in different environments without changing the code. | ||
|
||
== Implement business logic | ||
|
||
Let's create *customers/customer.py* file and create *Customer* class as follows: | ||
|
||
[source,python] | ||
---- | ||
include::{codebase}/customers/customer.py[lines="4..12"] | ||
---- | ||
|
||
Now, let's implement *create_table()* function to create *customers* table as follows: | ||
|
||
[source,python] | ||
---- | ||
include::{codebase}/customers/customer.py[lines="1..3,4..24"] | ||
---- | ||
|
||
We have obtained a new database connection using *get_connection()* function and created a *customers* table. | ||
We have used Python context manager *with* statement to automatically close the database connection once the table is created. | ||
|
||
Now, let's implement *create_customer()*, *get_all_customers()*, *get_customer_by_email()*, | ||
and *delete_all_customers()* functions as follows: | ||
|
||
[source,python] | ||
---- | ||
include::{codebase}/customers/customer.py[lines="26..54"] | ||
---- | ||
|
||
We have implemented various functions to insert, fetch, and delete customer records from the database | ||
using Python's DB-API. | ||
|
||
[NOTE] | ||
To keep it simple for the purpose of this guide, we are creating a new connection for every database operation. | ||
In a real-world application, it is recommended to use a connection pool to reuse connections. | ||
|
||
== Write tests using Testcontainers | ||
To test our functions, we will create a Postgres database in a container using Testcontainers once. | ||
Then we will use the same database for all the tests. | ||
Also, we will delete all the customer records before every test to run the tests in a predictable state. | ||
|
||
We are going to use pytest fixtures for implementing the setup and teardown logic. | ||
|
||
Let's create *tests/test_customers.py* file and implement the fixtures as follows: | ||
|
||
[source,python] | ||
---- | ||
include::{codebase}/tests/test_customers.py[lines="1..26"] | ||
---- | ||
|
||
We have used *module* scoped fixture to create a PostgreSQL container using Testcontainers. | ||
In the *setup()* fixture function, all the statements before *yield* will be executed before running any test. | ||
All the statements after *yield* will be executed after running all the tests in the module. | ||
|
||
In the *setup_data()* fixture function, we are deleting all the records in the *customers* table. | ||
This is a *function* scoped fixture, which will be executed before running every test. | ||
|
||
Now let's implement the tests as follows: | ||
|
||
[source,python] | ||
---- | ||
include::{codebase}/tests/test_customers.py[lines="28..40"] | ||
---- | ||
|
||
* In the *test_get_all_customers()* test, we are inserting 2 customer records into the database, | ||
fetching all the existing customers, and asserting the number of customers. | ||
* In the *test_get_customer_by_email()* test, we are inserting a customer record into the database, | ||
fetching the customer by email, and asserting the customer details. | ||
|
||
As we are deleting all the customer records before every test, the tests can be run in any order. | ||
|
||
== Run tests | ||
To enable the Pytest https://pytest.org/explanation/goodpractices.html#test-discovery[auto-discovery] mechanism, create *__init__.py* file under *tests* directory with empty content. | ||
|
||
Now let's run the tests using pytest as follows: | ||
|
||
[source,shell] | ||
---- | ||
$ pytest | ||
---- | ||
|
||
You should see the following output: | ||
|
||
[source,shell] | ||
---- | ||
pytest | ||
=============================== test session starts =============================== | ||
platform darwin -- Python 3.12.0, pytest-7.4.3, pluggy-1.3.0 | ||
rootdir: /Users/siva/dev/tc-python-demo | ||
collected 2 items | ||
tests/test_customers.py .. [100%] | ||
================================ 2 passed in 3.02s ================================ | ||
---- | ||
|
||
== Conclusion | ||
|
||
We have explored how to use *testcontainers-python* library for testing a Python application using a PostgreSQL database. | ||
In addition to PostgreSQL, testcontainers-python provides dedicated modules to many commonly used SQL databases, NoSQL databases, messaging queues, etc. | ||
You can use Testcontainers to run any containerized dependency for your tests! | ||
|
||
You can explore more about Testcontainers at https://www.testcontainers.com/. | ||
|
||
== Further Reading | ||
* https://testcontainers.com/guides/getting-started-with-testcontainers-for-java/[Getting started with Testcontainers for Java] | ||
* https://testcontainers.com/guides/getting-started-with-testcontainers-for-dotnet/[Getting started with Testcontainers for .NET] | ||
* https://testcontainers.com/guides/getting-started-with-testcontainers-for-go/[Getting started with Testcontainers for Go] | ||
* https://testcontainers.com/guides/getting-started-with-testcontainers-for-nodejs/[Getting started with Testcontainers for Node.js] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
certifi==2023.11.17 | ||
charset-normalizer==3.3.2 | ||
docker==6.1.3 | ||
idna==3.6 | ||
iniconfig==2.0.0 | ||
packaging==23.2 | ||
pluggy==1.3.0 | ||
psycopg==3.1.14 | ||
psycopg2-binary==2.9.9 | ||
pytest==7.4.3 | ||
requests==2.31.0 | ||
SQLAlchemy==2.0.23 | ||
testcontainers-core==0.0.1rc1 | ||
testcontainers-postgres==0.0.1rc1 | ||
typing_extensions==4.8.0 | ||
urllib3==2.1.0 | ||
websocket-client==1.7.0 | ||
wrapt==1.16.0 |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import os | ||
import pytest | ||
from testcontainers.postgres import PostgresContainer | ||
|
||
from customers import customers | ||
|
||
postgres = PostgresContainer("postgres:16-alpine") | ||
|
||
|
||
@pytest.fixture(scope="module", autouse=True) | ||
def setup(): | ||
postgres.start() | ||
os.environ["DB_HOST"] = postgres.get_container_host_ip() | ||
os.environ["DB_PORT"] = postgres.get_exposed_port(5432) | ||
os.environ["DB_USERNAME"] = postgres.POSTGRES_USER | ||
os.environ["DB_PASSWORD"] = postgres.POSTGRES_PASSWORD | ||
os.environ["DB_NAME"] = postgres.POSTGRES_DB | ||
customers.create_table() | ||
yield | ||
postgres.stop() | ||
|
||
|
||
@pytest.fixture(scope="function", autouse=True) | ||
def setup_data(): | ||
customers.delete_all_customers() | ||
|
||
|
||
def test_get_all_customers(): | ||
customers.create_customer(customers.Customer(0, "Siva", "[email protected]")) | ||
customers.create_customer(customers.Customer(0, "James", "[email protected]")) | ||
customers_list = customers.get_all_customers() | ||
assert len(customers_list) == 2 | ||
|
||
|
||
def test_get_customer_by_email(): | ||
customers.create_customer(customers.Customer(0, "John", "[email protected]")) | ||
customer = customers.get_customer_by_email("[email protected]") | ||
assert customer.name == "John" | ||
assert customer.email == "[email protected]" |