Skip to main content

Create test fixtures against an SQL database

Project description

Create database entries for your test scripts.

Example usage

First, set up a SQLite database. This should work with almost any SQL database. We’re using sqlite3 because it is built in.

import sqlite3  # or any db-api compatible database

conn = sqlite3.connect(":memory:")
conn.execute(
    """
    CREATE TABLE users (id INTEGER PRIMARY KEY, name text, active int)
    """
)
conn.execute(
    """
    CREATE TABLE groups (id INTEGER PRIMARY KEY, name text)
    """
)
conn.execute(
    """
    CREATE TABLE group_members (user_id INT, group_id INT)
    """
)

Now import sqlfixtures and create a sqlfixtures.SQLFixture object:

import sqlfixtures
fix = sqlfixtures.SQLFixture(conn)

Inserting data directly

Inserting data from dicts is the simplest way to start using sqlfixtures:

# Create a SQLFixture object to work with
fix = SQLFixture(dbconn)

# Roll back all rows inserted at the end of the test
with fix.rollback():

    # Insert rows
    fix.insert("users", {"name": "Angus"}) as user:
    assert user.name == "Angus"
    print("Inserted user", user)

This outputs:

Inserted user {'id': 1, 'name': 'Angus', 'active': None}

Updating works in a similar way:

fix.insert("users", {"name": "Angus"}) as user:
assert user.name == "Angus"
fix.update("users", {"name": "Alice"}, where={"id": user.id}) as user:
assert user.name == "Alice"
print(user)

Outputting:

{'id': 1, 'name': 'Alice', 'active': None}

Using the declarative API allows more expressivity:

angus = sqlfixtures.Insertable("users", name="angus", active=1)
alice = sqlfixtures.Insertable("users", name="alice", active=1)
order = sqlfixtures.Insertable("orders", user_id=angus.id)

fix = SQLFixtures(dbconn)
with fix.apply([angus, alice, order]) as items:
    assert items[0].name == "Angus"
    assert items[1].name == "Alice"

Reverting fixtures

Very often when using data fixtures, you want to revert the database back to the original state afterwards. SQLFixtures offers a few strategies for this:

fix = SQLFixture(dbconn)

with fix.undo():
    fix.insert(...)

with fix.rollback():
    fix.insert(...)

with fix.savepoint():
    fix.insert(...)

with fix.commit():
    fix.insert(...)

The shortcut method SQLFixture.apply allows you to apply insertable fixtures with a revert strategy:

alice = sqlfixtures.Insertable("users", name="alice", active=1)
fix = SQLFixture(dbconn)
with fix.apply(alice, "undo", strict=False):
    ...

Each revert strategy takes a strict argument, defaulting to True, causing errors encountered while reverting the fixture to raise an exception. If False, any errors will be logged only and not raised.

SQLFixtures.rollback([strict=True])

Roll back the database transaction on exit.

Use this if your application code can run in the same transaction as the fixture setup.

SQLFixtures.savepoint([strict=True])

Create a database savepoint on start and roll it back on exit. This is similar to rollback(), except that the transaction can continue outside of the fixture.

Use this if your application code can run in the same transaction as the fixture setup.

SQLFixtures.undo([strict=True])

Generate inverse operations for every statement run. For example every record inserted will result in a corresponding ‘DELETE’ statement being issued. This is more fragile (because it relies on matching the exact rows inserted, so changes to those rows will cause it to fail), but works independently of any database transactions.

Use this if your application code expects to commit or rollback the database transaction

SQLFixtures.commit([strict=True])

A variant on undo which commits the database transaction after insertion and again after cleanup.

Use this if your application commits or rolls back the transaction and you don’t want fixture data to be affected, or if you need fixture data to be visible from other database connections.

rollback and savepoint generally the safest to use, however they will break if your application commits or rolls back the transaction before sqlfixtures exits.

SQLFixtures.undo attempts to identify rows based on matching the entire inserted row. If your application modifies any field of your inserted fixture data, the reverter will no longer be able to identify the inserted fixture to remove it again. You can work around this by specifying pkcols in Insertable:

user = Insertable("users", pkcols=["id"], name="Bob", email="bob@example.com")

Now SQLFixtures will use the value of the id field when reverting the fixture with undo.

Custom revert code

Sometimes it is useful to revert objects that your application creates, which can be done by calling add_revert:

.. code:: python

fix = SQLFixture(dbconn)

with fix.apply(Insertable(“users”, username=”test_user”), “undo”):

fix.add_revert(“DELETE FROM posts”)

The declarative API - sqlfixture.Insertable

Using sqlfixtures.Insertable allows more expressivity:

angus = sqlfixtures.Insertable("users", name="angus", active=1)
alice = sqlfixtures.Insertable("users", name="alice", active=1)
order = sqlfixtures.Insertable("orders", user_id=angus.id)

fix = SQLFixtures(dbconn)
with fix.apply([angus, alice, order]) as items:
    assert items[0].name == "Angus"
    assert items[1].name == "Alice"

Pass multiple Insertables as a list, a dict, or an object graph of Insertables.

The return type will match the type passed in. If you pass a dict, you will get back a dict populated with the inserted data.

with fix.apply({"angus": angus, "alice": alice}) as users:
    print(users)

# users is a dict containing the inserted data:
{
    "angus": {'id': 1, 'name': 'Angus', 'active': 1},
    "alice": {'id': 2, 'name': 'Alice', 'active': 1}
}

Referencing inserted ids to create foreign key relationships

The declarative API allows you to reference columns that are populated by the database, for example using an auto-increment id field as a foreign key in another table:

administrators = sqlfixtures.Insertable("groups", name="administrators")
alice_is_admin = sqlfixtures.Insertable(
    "group_members", user_id=alice.id, group_id=administrators.id
)
with fix.apply([alice, administrators, alice_is_admin]) as rows:

    # The row inserted by `alice_is_admin` will contain the database-generated
    # keys for user_id and group_id
    # [
    #     {'id': 1, 'name': 'Alice', 'active': 1},
    #     {'id': 1, 'name': 'administrators'},
    #     {'user_id': 1, 'group_id': 1}
    # ]

    assert rows[2].user_id == rows[0].id
    assert rows[2].group_id == rows[1].id

Using Insertables as templates

Once an insertable object has been created it can be copied and customized just by calling it:

User = sqlfixtures.Insertable("users", active=1)
angus = User(name="Angus")
alice = User(name="Alice")
with fix.apply([angus, alice]) as users:
    assert users[0].name == "Angus"
    assert users[1].name == "Alice"
    print(users)
[{'id': 1, 'name': 'Angus', 'active': 1}, {'id': 2, 'name': 'Alice', 'active': 1}]

Generating values from callables

Values can also be set from callable objects. This is useful when using insertables as templates:

names = iter(["alice", "bob", "carol"])
User = sqlfixtures.Insertable("users", active=1, name=lambda: next(names))
with fix.apply([User(), User(), User()]) as users:
    assert users[0].name == "alice"
    assert users[1].name == "bob"
    assert users[2].name == "carol"

The callable may reference other properties through Reference arguments:

from sqlfixtures import SELF
names = iter(["alice", "bob", "carol"])
user = sqlfixtures.Insertable(
    "users",
    name=lambda: next(names),
    email=lambda name=SELF.name: f"{name}@example.com"
)

If the callable takes a single argument with no default value, the insertable itself will be substituted:

user = sqlfixtures.Insertable(
    "users",
    name="fred",
    email=lambda this_user: this_user.name
)

Nesting Insertables

Insertables can contain other insertables, to populate related tables. One-to-one relationships like this:

angus = sqlfixtures.Insertables(
    "users",
    name="Angus",
    address_id=sqlfixtures.Insertables(
        "addresses",
        line1="1 Anyroad",
        line2="Aberdeen"
    ).id
)

One-to-many like this:

alice = sqlfixtures.Insertables(
    "users",
    name="Alice",
    pets=[
        sqlfixtures.Insertables(
            "pets",
            user_id=sqlfixtures.PARENT.id,
            name="Zebedee",
            pet_type="zebra"
        )
    ]
)

Notice how the id is referenced differently in the two cases.

  • Insertables that are nested inside other insertables can reference the parent’s id using the special PARENT object (user_id=sqlfixtures.PARENT.id)

  • The container can directly reference the id of the child Insertable (pet_id=sqlfixtures.Insertable(...).id).

Using nested insertables as templates

Use a double underscore (__) to reference attributes of child insertables when creating an Insertable from a template:

User = sqlfixtures.Insertable(
    "users",
    address=sqlfixtures.Insertable("addresses"),
    address_id=address.id
)
angus = User(name="Angus", address__line1="1 Anyroad")

Insertable.after: breaking circular dependencies

Circular dependencies occur where you have 2 or more tables with foreign keys that reference each other. In this case you need to use Insertable.after to break the loop.

For example, the following code causes an unresolvable loop:

Insertable(
    "foo",
    bar_id=Insertable("bar", foo_id=PARENT.id).id
)

This can be rewritten so as to break the loop:

Insertable(
    "foo",
    bar=Insertable("bar", foo_id=PARENT.id)
).after(bar_id=SELF.bar.id)

The after method causes an UPDATE to be run after both records are inserted, somthing like this:

INSERT INTO foo (bar_id) VALUES (NULL) RETURNING foo_id
INSERT INTO bar (foo_id) VALUES (<foo_id>) RETURNING bar_id
UPDATE foo SET bar_id = <bar_id> WHERE foo_id = <foo_id>

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sqlfixtures-0.0.3.tar.gz (25.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sqlfixtures-0.0.3-py3-none-any.whl (21.8 kB view details)

Uploaded Python 3

File details

Details for the file sqlfixtures-0.0.3.tar.gz.

File metadata

  • Download URL: sqlfixtures-0.0.3.tar.gz
  • Upload date:
  • Size: 25.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for sqlfixtures-0.0.3.tar.gz
Algorithm Hash digest
SHA256 5e6bc6521143c54231beb701a96af540ce015c1bc431c025d48c9738e1f2b492
MD5 cda59436e50e35026fd6510d92bc6292
BLAKE2b-256 65692268c0a39d5a7c0d6ce64a5d505651a747c30541215029412653dbc93e00

See more details on using hashes here.

File details

Details for the file sqlfixtures-0.0.3-py3-none-any.whl.

File metadata

  • Download URL: sqlfixtures-0.0.3-py3-none-any.whl
  • Upload date:
  • Size: 21.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for sqlfixtures-0.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 d65e007c21bd169f6f19199e7b9173746f4b239d67c2720e2bf56ccb6d162cb9
MD5 accef60b2b11297dd8de1924eb494b80
BLAKE2b-256 360f40acaaa008be7eeca7692727ab28de18383a164458e4272457e09c0fd51f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page