7

I need to test a function with different parameters, and the most proper way for this seems to be using the with self.subTest(...) context manager.

However, the function writes something to the db, and it ends up in an inconsistent state. I can delete the things I write, but it would be cleaner if I could recreate the whole db completely. Is there a way to do that?

Ibolit
  • 9,218
  • 7
  • 52
  • 96

2 Answers2

2

Not sure how to recreate the database in self.subTest() but I have another technique I am currently using which might be of interest to you. You can use fixtures to create a "snapshot" of your database which will basically be copied in a second database used only for testing purposes. I currently use this method to test code on a big project I'm working on at work.

I'll post some example code to give you an idea of what this will look like in practice, but you might have to do some extra research to tailor the code to your needs (I've added links to guide you).

The process is rather straighforward. You would be creating a copy of your database with only the data needed by using fixtures, which will be stored in a .yaml file and accessed only by your test unit.

Here is what the process would look like:

  1. List item you want to copy to your test database to populate it using fixtures. This will only create a db with the needed data instead of stupidly copying the entire db. It will be stored in a .yaml file.

generate.py

    django.setup()
    stdout = sys.stdout

    conf = [
        {
            'file': 'myfile.yaml',
            'models': [
                dict(model='your.model', pks='your, primary, keys'),
                dict(model='your.model', pks='your, primary, keys')
            ]
        }
    ]

    for fixture in conf:
        print('Processing: %s' % fixture['file'])
        with open(fixture['file'], 'w') as f:
            sys.stdout = FixtureAnonymiser(f)

        for model in fixture['models']:
            call_command('dumpdata', model.pop('model'), format='yaml',indent=4, **model)
            sys.stdout.flush()

        sys.stdout = stdout
  1. In your test unit, import your generated .yaml file as a fixture and your test will automatically use this the data from the fixture to carry out the tests, keeping your main database untouched.

test_class.py

from django.test import TestCase

class classTest(TestCase):

    fixtures = ('myfile.yaml',)

    def setUp(self):
        """setup tests cases"""
       # create the object you want to test here, which will use data from the fixtures

    def test_function(self):
        self.assertEqual(True,True)
        # write your test here

You can read up more here:

If you have any questions because things are unclear just ask, I'd be happy to help you out.

Adam Jaamour
  • 1,326
  • 1
  • 15
  • 31
  • 1
    Thank you for the reply. I will need some time to take a close look at it. I will accept it as soon as I understand it. – Ibolit Sep 07 '17 at 15:48
  • Yes of course man, again it is quite confusing until you've seen the full code working properly, so any questions and I'll try to guide you in the right direction. – Adam Jaamour Sep 07 '17 at 16:04
  • @Ibolit did you manage to work something out or find another solution? – Adam Jaamour Sep 19 '17 at 10:59
  • I ended up deleting everything from the db manually, but I will test this solution as well. Sorry for taking too long. – Ibolit Sep 19 '17 at 12:02
  • 1
    I was using fixtures to setup my test database but after a big refactoring I was no longer able to load the fixtures. After having a lot of trouble with fixtures I decided to create a `setup_db` function and call it at the top of my test functions. Also I'm using `faker` package to generate fake data. As of now, this method works for me the best. – A.Mohammadi Aug 01 '21 at 02:49
  • @A.Mohammadi glad this was helpful! – Adam Jaamour Aug 02 '21 at 09:35
0

Maybe my solution will help someone

I used transactions to roll back to the database state that I had at the start of the test.

I use Eric Cousineau's decorator function to parametrizing tests

More about database transactions at django documentation page

import functools

from django.db import transaction
from django.test import TransactionTestCase
from django.contrib.auth import get_user_model

User = get_user_model()


def sub_test(param_list):
    """Decorates a test case to run it as a set of subtests."""

    def decorator(f):

        @functools.wraps(f)
        def wrapped(self):
            for param in param_list:
                with self.subTest(**param):
                    f(self, **param)

        return wrapped

    return decorator


class MyTestCase(TransactionTestCase):
    @sub_test([
        dict(email="new@user.com", password='12345678'),
        dict(email="new@user.com", password='password'),
    ])
    def test_passwords(self, email, password):
        # open a transaction
        with transaction.atomic():
            # Creates a new savepoint. Returns the savepoint ID (sid).
            sid = transaction.savepoint()
            
            # create user and check, if there only one with this email in DB
            user = User.objects.create(email=email, password=password)
            self.assertEqual(User.objects.filter(email=user.email).count(), 1)

            # Rolls back the transaction to savepoint sid.
            transaction.savepoint_rollback(sid)