Testing
All code generated by Imagine's SmartCompiler has 100% test coverage for end-to-end tests and unit tests
Imagine test coverage
Our generated code includes the following types of tests for various domains:
- object factories that can be used for building tests
- test that data models are serialized correctly
- test that an object can be created, read, updated and deleted from the storage
- test that an update of a field for an object with a value that has an incorrect type fails
- test that an update of a field for an object with a value that is outside the min or max allowed values should fail
- test that an update of a field which is not a part of the data fields of the API should fail
- test that related objects for an object can be updated with the API
- end to end tests with multiple API operations performed one after the other
Example tests
For GraphQL API
We share below some examples
import factory
import random
import json
from faker import Factory
from graphene_django.utils.testing import GraphQLTestCase
from graphene_django.utils.utils import camelize
from graphql_relay import to_global_id
from music.schema import AlbumNode, MusicianNode, SongNode
from music.models import Album
from .factories import MusicianFactory, AlbumFactory, SongFactory
faker = Factory.create()
class Album_CRUD_Test(GraphQLTestCase):
def setUp(self):
self.GRAPHQL_URL = "/music/graphql"
AlbumFactory.create_batch(size=3)
def test_create_album(self):
"""
Ensure we can create a new album object.
"""
album_dict = camelize(factory.build(dict, FACTORY_CLASS=AlbumFactory))
album_dict.pop('musician')
musician = MusicianFactory.create()
musician_graphql_id = to_global_id(MusicianNode._meta.name, musician.pk)
response = self.query(
"""
mutation($input: CreateAlbumInput!) {
createAlbum(input: $input) {
clientMutationId,
album {
id
name
releaseDate
numStars
}
}
}
""",
input_data={'data': album_dict, 'musicianId': musician_graphql_id}
)
content = json.loads(response.content)
generated_album = content['data']['createAlbum']['album']
self.assertResponseNoErrors(response)
for key in album_dict.keys():
self.assertEquals(album_dict[key], generated_album[key])
def test_create_nested_album(self):
"""
Ensure we can create a new album object with nested data.
Generate an album object and an songs objects using factories. Then, use
the createAlbum mutation to create both album and songs in the same API call.
"""
songs_dict = camelize(factory.build_batch(dict, FACTORY_CLASS=SongFactory, size=3))
for song in songs_dict:
song.pop('album')
album_dict = camelize(factory.build(dict, FACTORY_CLASS=AlbumFactory, songs=songs_dict))
album_dict.pop('musician')
musician = MusicianFactory.create()
musician_graphql_id = to_global_id(MusicianNode._meta.name, musician.pk)
response = self.query(
"""
mutation createAlbumWithSongs($input: CreateAlbumInput!) {
createAlbum(input: $input)
{
clientMutationId,
album {
id
songs{
edges{
node{
name
lyrics
}
}
}
}
}
}
""",
input_data={'data': album_dict, 'musicianId': musician_graphql_id}
)
self.assertResponseNoErrors(response)
def test_fetch_all(self):
"""
Create 3 objects, fetch all using allAlbums query and check that the 3 objects are returned following
Relay standards.
"""
response = self.query(
"""
query {
allAlbums{
edges{
node{
name
releaseDate
numStars
}
}
}
}
"""
)
self.assertResponseNoErrors(response)
content = json.loads(response.content)
albums = content['data']['allAlbums']['edges']
albums_qs = Album.objects.all()
for i, edge in enumerate(albums):
album = edge['node']
self.assertEquals(album['name'], albums_qs[i].name)
self.assertEquals(album['releaseDate'], str(albums_qs[i].release_date))
self.assertEquals(album['numStars'], albums_qs[i].num_stars)
def test_delete_mutation(self):
"""
Create 3 objects, fetch all using allAlbums query and check that the 3 objects are returned.
Then in a loop, delete one at a time and check that you get the correct number back on a fetch all.
"""
list_query = """
query {
allAlbums{
edges{
node{
id
}
}
}
}
"""
response = self.query(list_query)
self.assertResponseNoErrors(response)
content = json.loads(response.content)
albums = content['data']['allAlbums']['edges']
album_count = len(albums)
for i, edge in enumerate(albums, start=1):
album = edge['node']
album_id = album['id']
response = self.query(
"""
mutation($input:DeleteMusicianInput!) {
deleteMusician(input: $input)
{
ok
}
}
""", input_data={'id': album_id})
response = self.query(list_query)
content = json.loads(response.content)
albums = content['data']['allAlbums']['edges']
new_len = len(albums)
assert album_count - i == new_len
def test_delete_related_songs(self):
"""
Create an album object with some associated songs. Then perform an update query to remove some of them.
"""
album = AlbumFactory.create()
album_graphql_id = to_global_id(AlbumNode._meta.name, album.pk)
songs = SongFactory.create_batch(album=album, size=3)
songs_id_list = list(map(lambda song: to_global_id(SongNode._meta.name, song.pk), songs))
response = self.query(
"""
mutation($input:UpdateAlbumInput!){
updateAlbum(input: $input){
album{
songs{
edges{
node{
id
}
}
}
}
}
}
""",
input_data={'data': {'songsRemove': songs_id_list[:1]}, 'id': album_graphql_id}
)
self.assertResponseNoErrors(response)
parsed_response = json.loads(response.content)
response_data = parsed_response['data']['updateAlbum']
songs_list_graphql = response_data['album']['songs']['edges']
songs_id_list_graphql = []
for song_node in songs_list_graphql:
songs_id_list_graphql.append(song_node['node']['id'])
self.assertEquals(songs_id_list[1:], songs_id_list_graphql)
def test_update_mutation_correct(self):
"""
Add an object. Call an update with 2 (or more) fields updated.
Fetch the object back and confirm that the update was successful.
"""
album = AlbumFactory.create()
album_id = to_global_id(AlbumNode._meta.name, album.pk)
album_dict = factory.build(dict, FACTORY_CLASS=AlbumFactory)
response = self.query(
"""
mutation($input: UpdateAlbumInput!){
updateAlbum(input: $input) {
album{
name
numStars
}
}
}
""",
input_data={
'id': album_id,
'data': {
'name': album_dict['name'],
'numStars': album_dict['num_stars']
}
}
)
self.assertResponseNoErrors(response)
response = self.query(
"""
query($id: ID!) {
album(id:$id){
name
numStars
}
}
""",
variables={'id': album_id})
parsed_response = json.loads(response.content)
updated_album_data = parsed_response['data']['album']
self.assertEquals(updated_album_data['name'], album_dict['name'])
self.assertEquals(updated_album_data['numStars'], album_dict['num_stars'])
def test_update_mutation_incorrect(self):
"""
Add an object. Call an update with 2 (or more) fields updated with values that are expected to fail.
Fetch the object back and confirm that the fields were not updated (even partially).
"""
album = AlbumFactory.create()
album_id = to_global_id(AlbumNode._meta.name, album.pk)
random_int = faker.pyint(min_value=21)
response = self.query(
"""
mutation{
updateAlbum(input: {
id: "%s",
data:{
name: %s,
releaseDate: %s
}
}) {
album{
name
releaseDate
}
}
}
""" % (album_id, random_int, random_int)
)
self.assertResponseHasErrors(response)
def test_update_add_song_mutation(self):
"""
Add an album object without associated songs. Then, perform an update query to add multiple
songs.
"""
album = AlbumFactory.create()
album_id = to_global_id(AlbumNode._meta.name, album.pk)
# it's important to use camelize, to convert all snake case keys to camel case
songs_len = random.randint(1, 10)
songs_dict = camelize(factory.build_batch(dict, FACTORY_CLASS=SongFactory, size=songs_len))
for song in songs_dict:
song.pop('album')
response = self.query(
"""
mutation($input:UpdateAlbumInput!){
updateAlbum(input:$input){
album{
songs{
edges{
node{
id
}
}
}
}
}
}
""",
input_data={'id': album_id, 'data': {'songsAdd': songs_dict}})
parsed_response = json.loads(response.content)
updated_album = parsed_response['data']['updateAlbum']['album']
retrieved_songs_len = len(updated_album['songs']['edges'])
self.assertResponseNoErrors(response)
self.assertEquals(songs_len, retrieved_songs_len)
For REST API
import json
import factory
from django.core import management
from django.test import TestCase
from django.urls import reverse
from faker import Factory
from rest_framework import status
from rest_framework.test import APIClient
from ..models import Album
from .factories import AlbumFactory, MusicianFactory, SongFactory
from .utils import generate_authenticated_api_client, generate_user
faker = Factory.create()
class Album_API_Test(TestCase):
def setUp(self):
self.api_client = APIClient()
AlbumFactory.create_batch(size=3)
self.musician = MusicianFactory.create()
management.call_command("initialize")
self.authenticated_api_client = generate_authenticated_api_client(
generate_user(
permissions=['my-custom-permission1']
)
)
def test_create_album(self):
"""
Ensure we can create a new album object.
"""
client = self.authenticated_api_client
album_count = Album.objects.count()
album_dict = factory.build(dict, FACTORY_CLASS=AlbumFactory, musician=self.musician.pk)
response = client.post(reverse('album-list'), album_dict)
created_album_pk = response.data['id']
assert response.status_code == status.HTTP_201_CREATED
assert Album.objects.count() == album_count + 1
album = Album.objects.get(pk=created_album_pk)
assert album.name == album_dict['name']
assert str(album.release_date) == album_dict['release_date']
assert album.num_stars == album_dict['num_stars']
def test_fetch_all(self):
"""
Create 3 objects, do a fetch all call and check if you get back 3 objects
"""
client = self.authenticated_api_client
response = client.get(reverse('album-list'))
assert response.status_code == status.HTTP_200_OK
assert Album.objects.count() == len(response.data)
def test_delete(self):
"""
Create 3 objects, do a fetch all call and check if you get back 3 objects.
Then in a loop, delete one at a time and check that you get the correct number back on a fetch all.
"""
client = self.authenticated_api_client
album_qs = Album.objects.all()
album_count = Album.objects.count()
list_url = reverse('album-list')
response = client.get(list_url)
assert response.status_code == status.HTTP_200_OK
assert album_count == len(response.data)
for i, album in enumerate(album_qs, start=1):
response = client.delete(reverse('album-detail', kwargs={'pk': album.pk}))
assert response.status_code == status.HTTP_204_NO_CONTENT
response = client.get(list_url)
assert album_count - i == len(response.data)
def test_update_correct(self):
"""
Add an object. Call an update with 2 (or more) fields updated.
Fetch the object back and confirm that the update was successful.
"""
client = self.authenticated_api_client
album_pk = Album.objects.first().pk
album_detail_url = reverse('album-detail', kwargs={'pk': album_pk})
album_dict = factory.build(dict, FACTORY_CLASS=AlbumFactory, musician=self.musician.pk)
response = client.patch(album_detail_url, data=album_dict)
assert response.status_code == status.HTTP_200_OK
assert album_dict['name'] == response.data['name']
assert album_dict['num_stars'] == response.data['num_stars']
assert album_dict['release_date'] == response.data['release_date']
def test_update_num_starts_with_incorrect_value_data_type(self):
client = self.authenticated_api_client
album_pk = Album.objects.first().pk
album_detail_url = reverse('album-detail', kwargs={'pk': album_pk})
album = client.get(album_detail_url).data
data = {
'num_stars': faker.pystr(),
}
response = client.patch(album_detail_url, data=data)
album_after_request = client.get(album_detail_url).data
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert album['num_stars'] == album_after_request['num_stars']
def test_update_first_name_with_incorrect_value_outside_constraints(self):
client = self.authenticated_api_client
album_pk = Album.objects.first().pk
album_detail_url = reverse('album-detail', kwargs={'pk': album_pk})
album = client.get(album_detail_url).data
data = {
'name': faker.pystr(min_chars=101, max_chars=101),
}
response = client.patch(album_detail_url, data=data)
album_after_request = client.get(album_detail_url).data
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert album['name'] == album_after_request['name']