Unittest con Django Rest Framework
¡Hey! Por fin llegamos a la última parte de esta serie de tutoriales para aprender a crear una API con Django REST Framework 🎉. En esta parte veremos como integrar test unitarios en nuestra API para comprobar que todo funciona correctamente.
Sin más dilación ¡Empezamos!
Lo primero de todo explicar un poco como van los tests. Los test de DRF extienden de los tests de Django y estos a su vez extienden de la librería unittest de Python. Lo bueno de los tests en Django es que para realizar las pruebas, Django crea una bbdd nueva con la que realizar los tests, de esta forma no se comprometen los datos que tenemos en la base de datos original, así que muy importante es que el usuario que utilizamos para conectarnos a la base de datos de nuestro proyecto sea un usuario con permisos para crear una base de datos, si no fallará el proceso. Otra cosa importante es que al finalizar el proceso se eliminará la base de datos al menos que le indiques lo contrario.
Os dejo los enlaces a las documentaciones para que les echéis un vistazo.
Test con Django REST Framework
Para el primer caso, vamos a testar el registro y login de usuario. Para ello abrimos el archivo el users/tests.py y añadimos el siguiente código:
users/tests.py
# Django
from django.test import TestCase
# Python
from PIL import Image
import tempfile
import json
# Django Rest Framework
from rest_framework.test import APIClient
from rest_framework import status
# Models
from users.models import User
class UserTestCase(TestCase):
def setUp(self):
user = User(
email='testing_login@cosasdedevs.com',
first_name='Testing',
last_name='Testing',
username='testing_login'
)
user.set_password('admin123')
user.save()
def test_signup_user(self):
"""Check if we can create an user"""
image = Image.new('RGB', (100, 100))
tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg')
image.save(tmp_file)
tmp_file.seek(0)
client = APIClient()
response = client.post(
'/users/signup/', {
'email': 'testing@cosasdedevs.com',
'password': 'rc{4@qHjR>!b`yAV',
'password_confirmation': 'rc{4@qHjR>!b`yAV',
'first_name': 'Testing',
'last_name': 'Testing',
'phone': '999888777',
'city': 'Madrid',
'country': 'España',
'photo': tmp_file,
'extract': 'I am a testing',
'username': 'testing1'
},
format='multipart'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(json.loads(response.content), {"username":"testing1","first_name":"Testing","last_name":"Testing","email":"testing@cosasdedevs.com"})
def test_login_user(self):
client = APIClient()
response = client.post(
'/users/login/', {
'email': 'testing_login@cosasdedevs.com',
'password': 'admin123',
},
format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
result = json.loads(response.content)
self.assertIn('access_token', result)
Las dos librerías que importamos para hacer los tests son TestCase de Django y APIClient de Django REST Framework.
La primera será la clase base de la que extendermos para poder usar los tests. Para que funcione correctamente el nombre de la clase debe terminar con TestCase y el formato del nombre debe ser CamelCase.
La clase APIClient la utilizaremos para poder realizar peticiones a nuestra propia API, esta clase nos permite realizar peticiones con todos los métodos, pasar cabeceras y parámetros.
Una vez declarada la clase, lo siguiente que haremos será crear el método setUp, este método se ejecutará antes de cada test creemos, en este caso lo que estamos haciendo es crear un usuario para cada test con el que haremos pruebas.
Bien, y ahora llega el primer test llamado test_signup_user. Importante, todos los test que creemos deben empezar por la palabra test_ y tener formato snake_case, si no no los ejecutará.
En este test vamos a intentar crear un usuario y posteriormente utilizaremos los métodos assertEqual de unittest. Para el que no lo sepa, los métodos assert son las funciones que se utilizan para validar las acciones que realizamos, en este caso las de la API. Aquí lo que hacemos es verificar que el código de respuesta del server es el correcto y que la respuesta que nos devuelve la API es la que esperamos. Si todo está bien validará el test y si no nos aparecerá un error.
Para ver toda la lista de métodos de unittest con los que podemos hacer pruebas, os dejo el enlace.
El siguiente test que vamos a realizar será test_login_user, este se encargará de realizar el login. Al igual que en el test anterior, comprobamos que el estado de la respuesta es el que esperamos y también comprobaremos que la respuesta de la API nos está devolviendo el token de acceso con el método assertIn.
Aquí he añadido dos test básicos pero también se pueden añadir test en los que nos devuelva errores la API para comprobar que funcionan por ejemplo las validaciones y comprobar el error cuando enviamos datos erróneos.
Ahora que ya tenemos unos cuantos tests, es hora de probarlo, para ejecutar los tests deberemos lanzarlos de la siguiente forma:
docker exec api_drf_curriculum_web_1 bash -c "python manage.py test"
Ahora vamos a crear los tests para testar la parte en la que añadíamos la educación en nuestro curriculum, para este caso vamos a crear una carpeta llamada tests dentro de education y dentro de ella crearemos el archivo tests_user.py, esto lo vamos a hacer así porque más adelante vamos a crear otro test en el que crearemos un usuario de tipo reclutador y verificaremos que no puede acceder a esta parte de la API.
education/tests/tests_user.py
# Django
from django.test import TestCase
# Python
import json
# Django Rest Framework
from rest_framework.test import APIClient
from rest_framework import status
# Models
from education.models import Education
from users.models import User
class EducationTestCase(TestCase):
def setUp(self):
# Creamos un usuario y generamos el acceso a la api para hacer pruebas de forma general
user = User(
email='testing_login@cosasdedevs.com',
first_name='Testing',
last_name='Testing',
username='testing_login'
)
user.set_password('admin123')
user.save()
client = APIClient()
response = client.post(
'/users/login/', {
'email': 'testing_login@cosasdedevs.com',
'password': 'admin123',
},
format='json'
)
result = json.loads(response.content)
self.access_token = result['access_token']
self.user = user
def test_create_education(self):
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + self.access_token)
test_education = {
'date_ini': '2010-09-01T19:41:21Z',
'date_end': '2012-09-01T19:41:21Z',
'title': 'Desarrollo de Aplicaciones informáticas',
}
response = client.post(
'/education/',
test_education,
format='json'
)
result = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertIn('pk', result)
self.assertIn('date_ini', result)
self.assertIn('date_end', result)
self.assertIn('title', result)
if 'pk' in result:
del result['pk']
self.assertEqual(result, test_education)
def test_update_education(self):
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + self.access_token)
# Creamos un objeto en la base de datos para trabajar con datos
edu = Education.objects.create(
date_ini='2010-09-01T19:41:21Z',
date_end='2012-09-01T19:41:21Z',
title='DAM',
user=self.user
)
test_education_update = {
'date_ini': '2010-09-02T19:41:21Z',
'date_end': '2012-09-02T19:41:21Z',
'title': 'DAA',
}
response = client.put(
f'/education/{edu.pk}/',
test_education_update,
format='json'
)
result = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_200_OK)
if 'pk' in result:
del result['pk']
self.assertEqual(result, test_education_update)
def test_delete_education(self):
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + self.access_token)
# Creamos un objeto en la base de datos para trabajar con datos
edu = Education.objects.create(
date_ini='2010-09-01T19:41:21Z',
date_end='2012-09-01T19:41:21Z',
title='DAM',
user=self.user
)
response = client.delete(
f'/education/{edu.pk}/',
format='json'
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
edu_exists = Education.objects.filter(pk=edu.pk)
self.assertFalse(edu_exists)
def test_get_education(self):
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + self.access_token)
Education.objects.create(
date_ini='2010-09-01T19:41:21Z',
date_end='2012-09-01T19:41:21Z',
title='DAM',
user=self.user
)
Education.objects.create(
date_ini='2008-09-01T19:41:21Z',
date_end='2010-09-01T19:41:21Z',
title='Bachiller',
user=self.user
)
response = client.get('/education/')
result = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(result['count'], 2)
for edu in result['results']:
self.assertIn('pk', edu)
self.assertIn('date_ini', edu)
self.assertIn('date_end', edu)
self.assertIn('title', edu)
break
Para este ejemplo estamos creando en setUp un usuario y posteriormente obteniendo su token acceso porque lo utilizaremos en cada test que creemos.
El primer test que tenemos es el de creación de educación, llamamos a la API y verificamos que el estado de la respuesta es el correcto, que nos envían todos los datos de respuesta que tenemos configurados y por último si la respuesta es igual a la información que enviamos eliminando el campo pk para que no nos dé problemas en la comparación.
Para el segundo test haremos una actualización, para ello primero creamos la educación y la actualizamos, comparamos que la respuesta del server sea la correcta y como en el caso anterior, eliminamos el campo pk y lo comparamos con lo enviado para confirmar que la actualización se hizo de forma correcta.
El tercer test será para probar la eliminación. Para ello la creamos y luego hacemos luego lanzamos el APIClient para hacer la eliminación. Comprobamos que el server devuelve el código correcto y después comprobamos que no existe en la bbdd.
Para el último testaremos el get. Creamos dos objetos y después hacemos una petición de tipo get, confirmamos el código de la petición y que el número de resultados es 2. Por último verificamos que devuelve los datos que queremos.
Para finalizar este tutorial, vamos a confirmar que los recruiter no pueden acceder a la parte de educación de la API, para ello creamos el archivo tests_recruiter.py y añadimos el siguiente código:
education/tests/tests_recruiter.py
# Django
from django.test import TestCase
# Python
import json
# Django Rest Framework
from rest_framework.test import APIClient
from rest_framework import status
# Models
from education.models import Education
from users.models import User
class EducationRecruiterTestCase(TestCase):
def setUp(self):
# Creamos un usuario reclutador para comprobar que no tiene acceso
user = User(
email='testing_login@cosasdedevs.com',
first_name='Testing',
last_name='Testing',
username='testing_login',
is_recruiter=True
)
user.set_password('admin123')
user.save()
client = APIClient()
response = client.post(
'/users/login/', {
'email': 'testing_login@cosasdedevs.com',
'password': 'admin123',
},
format='json'
)
result = json.loads(response.content)
self.access_token = result['access_token']
self.user = user
def test_create_education_recruiter(self):
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + self.access_token)
test_education = {
'date_ini': '2010-09-01T19:41:21Z',
'date_end': '2012-09-01T19:41:21Z',
'title': 'Desarrollo de Aplicaciones informáticas',
}
response = client.post(
'/education/',
test_education,
format='json'
)
result = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(result['detail'], 'You do not have permission to perform this action.')
def test_update_education(self):
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + self.access_token)
test_education_update = {
'date_ini': '2010-09-02T19:41:21Z',
'date_end': '2012-09-02T19:41:21Z',
'title': 'DAA',
}
response = client.put(
'/education/1/',
test_education_update,
format='json'
)
result = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(result['detail'], 'You do not have permission to perform this action.')
def test_delete_education(self):
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + self.access_token)
response = client.delete(
'/education/1/',
format='json'
)
result = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(result['detail'], 'You do not have permission to perform this action.')
def test_get_education(self):
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + self.access_token)
response = client.get('/education/')
result = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(result['detail'], 'You do not have permission to perform this action.')
Para este test al igual que en el anterior, en setUp crearemos un usuario y haremos el login para tener el token pero con la diferencia de que este usuario será de tipo reclutador.
En estos tests vamos a verificar la actualización, creación, listado y borrado y todos verificaremos que el estado e la respuesta de la API y si nos devuelve el mensaje avisándonos que no tenemos permisos para realizar esta acción.
Y listo, ya hemos visto algunos ejemplos de como crear tests para Django REST Framework y podemos dar por finalizado el tutorial.
Os dejo en enlace del proyecto en mi github por si queréis echarle un vistazo al proyecto.
Espero que este post te ayude y como siempre, te recomiendo seguirme en Twitter para estar al tanto de los nuevo contenido. Ahora también puedes seguirme en Instagram donde estoy subiendo tips, tutoriales en vídeo e información sobre herramientas para developers.
Por último os dejo mi guía para aprender a trabajar con APIs donde explico todo el funcionamiento de una API, el protocolo HTTP y veremos como construir una API con arquitectura REST.
Nos leemos 👋.