Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
sivaprasadreddy committed Dec 5, 2023
1 parent 0567e58 commit 09b033e
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 2 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/python.yml
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.idea
venv/
.pytest_cache/

1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
41 changes: 39 additions & 2 deletions README.md
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.
53 changes: 53 additions & 0 deletions customers/customers.py
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()
12 changes: 12 additions & 0 deletions db/connection.py
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 guide/getting-started-with-testcontainers-for-python/index.adoc
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]
18 changes: 18 additions & 0 deletions requirements.txt
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 added tests/__init__.py
Empty file.
39 changes: 39 additions & 0 deletions tests/test_customers.py
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]"

0 comments on commit 09b033e

Please sign in to comment.