How to use pytest fixtures with django TestCase - python

How can I use a pytest fixture within a TestCase method? Several answers to similar questions seem to imply that my example should work:
import pytest
from django.test import TestCase
from myapp.models import Category
pytestmark = pytest.mark.django_db
#pytest.fixture
def category():
return Category.objects.create()
class MyappTests(TestCase):
def test1(self, category):
assert isinstance(category, Category)
But this always results in an error:
TypeError: test1() missing 1 required positional argument: 'category'
I realize I could just convert this trivial example into a function, and it would work. I would prefer to use django's TestCase because it includes support for importing traditional "django fixture" files, which several of my tests require. Converting my tests to functions would require re-implementing this logic, since there isn't a documented way of importing "django fixtures" with pytest (or pytest-django).
package versions:
Django==3.1.2
pytest==6.1.1
pytest-django==4.1.0

I find it easier to use the "usefixtures" approach. It doesn't show a magical 2nd argument to the function and it explicitly marks the class for having fixtures.
#pytest.mark.usefixtures("category")
class CategoryTest(TestCase):
def test1(self):
assert Category.objects.count() == 1

I opted to rewrite django's fixture logic using a "pytest fixture" that is applied at the session scope. All you need is a single fixture in a conftest.py file at the root of your test directory:
import pytest
from django.core.management import call_command
#pytest.fixture(scope='session')
def django_db_setup(django_db_setup, django_db_blocker):
fixtures = [
'myapp/channel',
'myapp/country',
...
]
with django_db_blocker.unblock():
call_command('loaddata', *fixtures)
This allowed me to throw out the class-based tests altogether, and just use function-based tests.
docs

Why do you need TestCase? I usually use Python class and create tests there.
Example
import pytest
from django.urls import reverse
from rest_framework import status
from store.models import Book
from store.serializers import BooksSerializer
#pytest.fixture
def test_data():
"""Поднимает временные данные."""
Book.objects.create(name='Book1', price=4000)
#pytest.fixture
def api_client():
"""Возвращает APIClient для создания запросов."""
from rest_framework.test import APIClient
return APIClient()
#pytest.mark.django_db
class TestBooks:
#pytest.mark.usefixtures("test_data")
def test_get_books_list(self, api_client):
"""GET запрос к списку книг."""
url = reverse('book-list')
excepted_data = BooksSerializer(Book.objects.all(), many=True).data
response = api_client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.data == excepted_data
assert response.data[0]['name'] == Book.objects.first().name

Related

Mocking django tests - why is this patched function still being called?

I'm trying to test a function in one of my models, and am trying to to mock out the filesytem using mock.patch. No matter what I try, it doesn't seem to intercept the method.
Model to test:
app/models.py
from django.db import models
from .utils.storageutils import get_file
from .utils.datautils import derive_data
Class DataThing(models.Model):
#defined here
def set_data_from_file(self):
data = derive_data(get_file('filepath'))
setattr(self, 'derived_data', data)
self.save()
app/utils/datautils.py
import pandas as pd
def derive_data(data_from_file):
df = pd.DataFrame('...')
#do stuff with dataframe
return df
app/tests/unit_tests/tests_models.py
from django.test import TestCase
import mock
from app.models import DataThing
class DataThingModelTest(TestCase):
#classmethod
def setUpTestData(cls):
cls.datathing = DataThing
#mock.patch('app.models.derive_data')
def test_set_data_from_file(self, mocked_derive_data):
mocked_derive_data.return_value=('pretend_dataframe')
self.datathing.set_data_from_file()
self.assertEquals(self.datathing.derived_data, 'pretend_dataframe')
I would expect this to pass. However, I get an error, because datathing.set_data_from_file() is still ultimately calling utils.storageutils.get_file. I've tried patching in app.utils.datautils.derive_data but have the same issue.
I also needed to patch the get_file function
#mock.patch('app.models.get_file')
#mock.patch('app.models.derive_data')
def test_set_data_from_file(self, mocked_derive_data, mocked_ger_file):
mocked_get_file.return_value=('placeholder')
mocked_derive_data.return_value=('pretend_dataframe')
self.datathing.set_data_from_file()
self.assertEquals(self.datathing.derived_data, 'pretend_dataframe')

Mocking class variable with mocker in pytest

I have a problem roughly looking like this:
In a file data.py I have
from typing import ClassVar
from tinydb import TinyDB
from dataclasses import dataclass
#dataclass
class Data:
db: ClassVar = TinyDB("some_path")
#property
def some_data(self):
return 100
I would like to mock the some_data method.
I tried:
import pytest
import pandas as pd
from package1.data import Data
#pytest.fixture
def mocked_raw_data(mocker):
m = mocker.patch.object(
Data, "some_data", return_value=10, new_callable=mocker.PropertyMock
)
)
return m
def test_some_data(mocked_raw_data):
assert Data().some_data == 2
But obviously this gives an error with the db method class variable. How can I mock this variable as well? Does my approach generally make sense?
Did you use #pytest.mark.django_db?
This would help in testing data on a separate DB rather than the production one.
And regarding your question on mocking, you can use monkey patch for mocking
For eg,
def test_user_details(monkeypatch):
mommy.make('Hallpass', user=user)
return_data =
{
'user_created':'done'
}
monkeypatch.setattr(
'user.create_user', lambda *args, **kwargs: return_data)
user_1 = create_user(user="+123456789")
assert user_1.return_data == return_data

Pytest: Inherit fixture from parent class

I have a couple of test cases to test the endpoints of a flask/connexion based api.
Now I want to reorder them into classes, so there is a base class:
import pytest
from unittest import TestCase
# Get the connexion app with the database configuration
from app import app
class ConnexionTest(TestCase):
"""The base test providing auth and flask clients to other tests
"""
#pytest.fixture(scope='session')
def client(self):
with app.app.test_client() as c:
yield c
Now I have another class with my actual testcases:
import pytest
from ConnexionTest import ConnexionTest
class CreationTest(ConnexionTest):
"""Tests basic user creation
"""
#pytest.mark.dependency()
def test_createUser(self, client):
self.generateKeys('admin')
response = client.post('/api/v1/user/register', json={'userKey': self.cache['admin']['pubkey']})
assert response.status_code == 200
Now unfortunately I always get a
TypeError: test_createUser() missing 1 required positional argument: 'client'
What is the correct way to inherit the fixture to subclasses?
So after googling for more infos about fixtures I came across this post
So there were two required steps
Remove the unittest TestCase inheritance
Add the #pytest.mark.usefixtures() decorator to the child class to actually use the fixture
In Code it becomes
import pytest
from app import app
class TestConnexion:
"""The base test providing auth and flask clients to other tests
"""
#pytest.fixture(scope='session')
def client(self):
with app.app.test_client() as c:
yield c
And now the child class
import pytest
from .TestConnexion import TestConnexion
#pytest.mark.usefixtures('client')
class TestCreation(TestConnexion):
"""Tests basic user creation
"""
#pytest.mark.dependency(name='createUser')
def test_createUser(self, client):
self.generateKeys('admin')
response = client.post('/api/v1/user/register', json={'userKey': self.cache['admin']['pubkey']})
assert response.status_code == 200

Change default faker locale in factory_boy

How can I set the default locale in Python's factory_boy for all of my Factories?
In docs says that one should set it with factory.Faker.override_default_locale but that does nothing to my fakers...
import factory
from app.models import Example
from custom_fakers import CustomFakers
# I use custom fakers, this indeed are added
factory.Faker.add_provider(CustomFakers)
# But not default locales
factory.Faker.override_default_locale('es_ES')
class ExampleFactory(factory.django.DjangoModelFactory):
class Meta:
model = Example
name = factory.Faker('first_name')
>>> from example import ExampleFactory
>>> e1 = ExampleFactory()
>>> e1.name
>>> u'Chad'
The Faker.override_default_locale() is a context manager, although it's not very clear from the docs.
As such, to change the default locale for a part of a test:
with factory.Faker.override_default_locale('es_ES'):
ExampleFactory()
For the whole test:
#factory.Faker.override_default_locale('es_ES')
def test_foo(self):
user = ExampleFactory()
For all the tests (Django):
# settings.py
TEST_RUNNER = 'myproject.testing.MyTestRunner'
# myproject/testing.py
import factory
from django.conf import settings
from django.util import translation
import django.test.runner
class MyTestRunner(django.test.runner.DiscoverRunner):
def run_tests(self, test_labels, extra_tests=None, **kwargs):
with factory.Faker.override_default_locale(translation.to_locale(settings.LANGUAGE_CODE)):
return super().run_tests(test_labels, extra_tests=extra_tests, **kwargs)
More on it here.
UPD As I said, this solution is suboptimal:
factory.Faker._DEFAULT_LOCALE is a private field
fake() and faker() use the private interface
fake() doesn't work since factory-boy==3.1.0
if I were to use faker, I'd use it directly, not via factory-boy
You should generally prefer the other answer. Leaving this one for posterity.
Not a good solution, but for now it's as good as it gets. You can change the variable that holds the value:
import factory
factory.Faker._DEFAULT_LOCALE = 'xx_XX'
Moreover, you can create a file like this (app/faker.py):
import factory
from faker.providers import BaseProvider
factory.Faker._DEFAULT_LOCALE = 'xx_XX'
def fake(name):
return factory.Faker(name).generate({})
def faker():
return factory.Faker._get_faker()
class MyProvider(BaseProvider):
def category_name(self):
return self.random_element(category_names)
...
factory.Faker.add_provider(MyProvider)
category_names = [...]
Then, once you import the file, the locale changes. Also, you get your providers and an easy way to use factory_boy's faker outside of the factories:
from app.faker import fake
print(fake('random_int'))
print(faker().random_int())
I'm having same issue as yours. For a temporary solution try passing locale in factory.Faker.
For example:
name = factory.Faker('first_name', locale='es_ES')
With Django, you can simply insert the following lines in <myproject>/settings.py:
import factory
factory.Faker._DEFAULT_LOCALE = 'fr_FR'
Further to #xelnor's answer, if using pytest (instead of Django manage.py test), add a hookwrapper on the pytest_runtestloop hook in your conftest.py to set the default locale for all the tests:
#pytest.hookimpl(hookwrapper=True)
def pytest_runtestloop(session):
with factory.Faker.override_default_locale(translation.to_locale(settings.LANGUAGE_CODE)):
outcome = yield

Django LiveServerTestCase: User created in in setUpClass method not available in test_method?

I am using Django 1.4's LiveServerTestCase for Selenium testing and am having trouble with the setUpClass class method. As far as I understand MembershipTests.setUpClass is run once before the unit tests are run.
I've put code to add a user to the database in MembershipTests.setUpClass but when I run the MembershipTests.test_signup test no user has been added to the test database. What am I doing incorrectly? I expect the user I created in setUpClass would be available across all unit tests.
If I put the user creation code in MembershipTests.setUp and run MembershipTests.test_signup I can see the user, but don't want this run before every unit test as setUp is. As you can see, I use a custom LiveServerTestCase class to add basic functionality across all of my tests (test_utils.CustomLiveTestCase). I suspect this has something to do with my issue.
Thanks in advance.
test_utils.py:
from selenium.webdriver.firefox.webdriver import WebDriver
from django.test import LiveServerTestCase
class CustomLiveTestCase(LiveServerTestCase):
#classmethod
def setUpClass(cls):
cls.wd = WebDriver()
super(CustomLiveTestCase, cls).setUpClass()
#classmethod
def tearDownClass(cls):
cls.wd.quit()
super(CustomLiveTestCase, cls).tearDownClass()
tests.py:
from django.contrib.auth.models import User
from django.test.utils import override_settings
from test_utils import CustomLiveTestCase
from test_constants import *
#override_settings(STRIPE_SECRET_KEY='xxx', STRIPE_PUBLISHABLE_KEY='xxx')
class MembershipTests(CustomLiveTestCase):
fixtures = [
'account_extras/fixtures/test_socialapp_data.json',
'membership/fixtures/basic/plan.json',
]
def setUp(self):
pass
#classmethod
def setUpClass(cls):
super(MembershipTests, cls).setUpClass()
user = User.objects.create_user(
TEST_USER_USERNAME,
TEST_USER_EMAIL,
TEST_USER_PASSWORD
)
def test_signup(self):
print "users: ", User.objects.all()
The database is torn down and reloaded on every test method, not on the test class. So your user will be lost each time. Do that in setUp not setUpClass.
Since you're using LiveServerTestCase it's almost same as TransactionTestCase which creates and destroys database (truncates tables) for every testcase ran.
So you really can't do global data with LiveServerTestCase.
You should be able to use TestCase.setUpTestData as follows (slight changes to your base class):
test_utils.py:
from selenium.webdriver.firefox.webdriver import WebDriver
from django.test import LiveServerTestCase, TestCase
class CustomLiveTestCase(LiveServerTestCase, TestCase):
#classmethod
def setUpClass(cls):
cls.wd = WebDriver()
super(CustomLiveTestCase, cls).setUpClass()
#classmethod
def tearDownClass(cls):
cls.wd.quit()
super(CustomLiveTestCase, cls).tearDownClass()
tests.py:
from django.contrib.auth.models import User
from django.test.utils import override_settings
from test_utils import CustomLiveTestCase
from test_constants import *
#override_settings(STRIPE_SECRET_KEY='xxx', STRIPE_PUBLISHABLE_KEY='xxx')
class MembershipTests(CustomLiveTestCase):
fixtures = [
'account_extras/fixtures/test_socialapp_data.json',
'membership/fixtures/basic/plan.json',
]
#classmethod
def setUpTestData(cls):
super(MembershipTests, cls).setUpTestData()
user = User.objects.create_user(
TEST_USER_USERNAME,
TEST_USER_EMAIL,
TEST_USER_PASSWORD
)
def test_signup(self):
print "users: ", User.objects.all()
Instead of changing the base class, you could inherit from TestCase in MembershipTests, but you'll have to do this everytime you need test data.
Note that I've also removed the def setUp: pass, as this will break the transaction handling.
Check out this thread for further details: https://groups.google.com/forum/#!topic/django-developers/sr3gnsc8gig
Let me know if you run into any issues with this solution!

Categories

Resources