Home > Software design >  Pytest-django - testing creation and passing a required User object
Pytest-django - testing creation and passing a required User object

Time:10-20

Apologies if this has already been answered elsewhere. I cannot find an answer which I can retrofit into my situation.

I'm new to django so I feel the problem is me not getting a fundamental grasp of a presumably basic concept here...

Using DRF and pytest-django, i'm trying to be diligent and write tests along the way before it becomes too time consuming to manually test. I can see it snowballing pretty quickly.

The issue I face is when I try to test the creation of a Catalogue, I can't get it to pass an User instance to the mandatory field 'created_by'. The logic works fine when I test manually, but writing the test itself is causing me headaches.

Many thanks in advance!

The error is: TypeError: Cannot encode None for key 'created_by' as POST data. Did you mean to pass an empty string or omit the value?

Code provided.

# core/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
    email = models.EmailField(unique=True)


# workshop/models.py
from django.conf import settings
from django.db import models

class Catalogue(models.Model):
    STATE_DRAFT = 'DRAFT'
    STATE_PUBLISHED_PRIVATE = 'PUBPRIV'
    STATE_PUBLISHED_PUBLIC = 'PUBPUB'

    STATE_CHOICES = [
        (STATE_DRAFT, 'Draft'),
        (STATE_PUBLISHED_PRIVATE, 'Published (Private)'),
        (STATE_PUBLISHED_PUBLIC, 'Published (Public)')
    ]
    company = models.ForeignKey(Company, on_delete=models.PROTECT)
    title = models.CharField(max_length=255)
    description = models.TextField(null=True, blank=True)
    state = models.CharField(
        max_length=10, choices=STATE_CHOICES, default=STATE_DRAFT)
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
    created_on = models.DateTimeField(auto_now_add=True)

    def __str__(self) -> str:
        return self.title

class CatalogueItem(models.Model):
    TYPE_GOODS = 'GOODS'
    TYPE_SERVICES = 'SERVICES'
    TYPE_GOODS_AND_SERVICES = 'GOODS_AND_SERVICES'

    TYPE_CHOICES = [
        (TYPE_GOODS, 'Goods'),
        (TYPE_SERVICES, 'Services'),
        (TYPE_GOODS_AND_SERVICES, 'Goods & Services')
    ]
    catalogue = models.ForeignKey(
        Catalogue, on_delete=models.CASCADE, related_name='catalogueitems')
    type = models.CharField(
        max_length=50, choices=TYPE_CHOICES, default=TYPE_GOODS)
    name = models.CharField(max_length=255)
    description = models.TextField()
    unit_price = models.DecimalField(max_digits=9, decimal_places=2)
    can_be_discounted = models.BooleanField(default=True)

    def __str__(self) -> str:
        return self.name

    @property
    def item_type(self):
        return self.get_type_display()


# workshop/serializers.py
class CatalogueSerializer(serializers.ModelSerializer):
    catalogueitems = SimpleCatalogueItemSerializer(
        many=True, read_only=True)
    created_on = serializers.DateTimeField(read_only=True)
    created_by = serializers.PrimaryKeyRelatedField(read_only=True)

    class Meta:
        # depth = 1
        model = Catalogue
        fields = ['id', 'title', 'description',
                  'state', 'catalogueitems', 'created_by', 'created_on']

    def create(self, validated_data):
        company_id = self.context['company_id']
        user = self.context['user']
        return Catalogue.objects.create(company_id=company_id, created_by=user, **validated_data)

# workshop/views.py
class CatalogueViewSet(ModelViewSet):
    serializer_class = CatalogueSerializer

    def get_permissions(self):
        if self.request.method in ['PATCH', 'PUT', 'DELETE', 'POST']:
            return [IsAdminUser()]
        return [IsAuthenticated()]

    def get_queryset(self):
        user = self.request.user
        if user.is_staff:
            return Catalogue.objects.prefetch_related('catalogueitems__catalogue').filter(company_id=self.kwargs['company_pk'])
        elif user.is_authenticated:
            return Catalogue.objects.filter(company_id=self.kwargs['company_pk'], state='PUBPUB')

    def get_serializer_context(self):
        company_id = self.kwargs['company_pk']
        return {'company_id': company_id, 'user': self.request.user}

# workshop/tests/conftest.py
from core.models import User
from rest_framework.test import APIClient
import pytest


@pytest.fixture
def api_client():
    return APIClient()


@pytest.fixture
def authenticate(api_client):
    def do_authenticate(is_staff=False):
        return api_client.force_authenticate(user=User(is_staff=is_staff))
    return do_authenticate


# workshop/tests/test_catalogues.py
from core.models import User
from workshop.models import Catalogue
from rest_framework import status
import pytest

@pytest.fixture
def create_catalogue(api_client):
    def do_create_catalogue(catalogue):
        return api_client.post('/companies/1/catalogues/', catalogue)
    return do_create_catalogue

class TestCreateCatalogue:
    def test_if_admin_can_create_catalogue_returns_201(self, authenticate,  create_catalogue):
        
        user = authenticate(is_staff=True)
        response = create_catalogue(
            {'title': 'a', 'description': 'a', 'state': 'DRAFT','created_by':user})

        assert response.status_code == status.HTTP_201_CREATED

CodePudding user response:

I think you may have a problem with the user that you are using to do the test,

when you call authenticate it returns a client which is not the same as a user.

then you run the authenticate and log in as a generic user. Try making another fixture that creates a user first, authenticate with that user to return the client and then post that user you created to create_catalogue

from django.conf import settings

@pytest.fixture
def create_user() -> User:
    return settings.AUTH_USER_MODEL.objects.create(
        username="Test User", password="Test Password", email="[email protected]"
    )

@pytest.fixture
def authenticate(api_client):
    def do_authenticate(create_user):
        return api_client.force_authenticate(create_user)
    return do_authenticate

class TestCreateCatalogue:
    def test_if_admin_can_create_catalogue_returns_201(self, authenticate, create_user  create_catalogue):
        
        user = authenticate(create_user)
        response = create_catalogue(
            {'title': 'a', 'description': 'a', 'state': 'DRAFT','created_by':create_user})

        assert response.status_code == status.HTTP_201_CREATED
  • Related