Codice sorgente per thx.tests.base_meta
# coding: utf-8
"""Test con Metaclassi
=========================
Questo modulo vuole offrire un metodo efficace per creare una moltitudine di test
partendo da una descrizione semplice e possibilmente efficace (dict o csv)
Il pacchetto offre quindi un paio di Metaclassi: una utile per form di dati
semplici per cui si usi ``application/json`` ed una per chiamate che richiedano
content-type ``multipart/form-data``.
L'utilizzo tipico sarà il seguente::
import os
from thx.tests.base_meta import TestUrlBaseMeta
from web.tests.base import BaseAPITest
from .test_specifications import test_dict
class AccountBaseMeta(TestUrlBaseMeta):
test_dict = test_dict
class AccountTest(BaseAPITest, metaclass=AccountBaseMeta):
def base_url_test(self, profile, action, url_name, args=None, data=None, return_code=None):
#print('AccountTest _test_page_base')
user = getattr(self, f"{profile}_user1")
self.client.force_authenticate(user)
AccountBaseMeta.base_url_test(self, profile, action, url_name, args, data, return_code)
O se si preferisce dichiarare i test in formato csv::
class AccountBaseMetaCSV(TestUrlBaseMeta):
csv_file = os.path.join(os.path.dirname(__file__), 'test_specification.csv')
class AccountTestCSV(BaseAPITest, metaclass=AccountBaseMetaCSV):
def base_url_test(self, profile, action, url_name, args=None, data=None, return_code=None):
#print('AccountTest _test_page_base')
user = getattr(self, f"{profile}_user1")
self.client.force_authenticate(user)
AccountBaseMetaCSV.base_url_test(self, profile, action, url_name, args, data, return_code)
È importante che l'attributo ``test_dict`` e rispettivamente ``csv_file`` siano definito
direttamente sulla metaclasse in quanto deve essere disponibile nel momento in cui viene creato
Qui si vede chiaramente l'utilizzo di test_dict che è una dizionario di cui diamo un esempio::
test_dict = [
{
'name': 'account_list_customer',
'profile': 'customer',
'action': 'list',
'url_name': 'api_account:owner:user_list',
'return_code': 403,
'help': 'Test: list, model:template, profile: customer'
},
{
'name': 'account_list_owner',
'profile': 'owner',
'action': 'list',
'url_name': 'api_account:owner:user_list',
'return_code': 200,
'help': 'Test: list, model:template, profile: customer'
}
]
Si veda la metaclasse per la descrizione del significato.
.. autoclass:: TestUrlBaseMeta
:members:
.. autoclass:: TestUrlMultipartMeta
:members:
"""
import csv
from collections import namedtuple
from django.urls import NoReverseMatch, reverse
[documenti]class TestUrlBaseMeta(type):
"""
Metaclasse idonea per generare test da una descrizione via dizionario
:name: il nome del test, la metaclasse aggiungerà ``test_`` come prefisso
:profile: il nome del profilo per il quale si crea il tests
:action: la ``action``:
* length
* list
* retrieve
* create
* update
* destroy
* status_set
* status_reset
* enable
* disable
* fail
:url_name: il name su cui viene fatta la ``resolve`` da Django
:return_code: lo status http atteso
:help: la doctring usata per il metodo di test, utile con l'opzione verbose o in caso di errori
"""
#: dict that holds all info to create a test
test_dict = {}
#: csv file con i test. test_dict o csv_file sono richiesti
csv_file = None
#: lista dei campi del file csv. I nomi non devono essere cambiati
csv_field_list = ('name', 'profile', 'action', 'url_name', 'args', 'data', 'return_code', 'help')
#: delimitatore di campi per il file csv
csv_delimiter = ';'
def __new__(mcs, name, bases, attrs):
def create_func(test_name, help_text, input_args, expected_value):
def func(self):
self.base_url_test(*input_args, return_code=expected_value)
func.__name__ = f"test_{test_name}"
func.__doc__ = help_text
return func
if mcs.csv_file:
test_data = mcs.get_data_from_csv()
else:
test_data = mcs.get_test_from_dict()
for test_name, help_text, required_args, expected_value in test_data:
func = create_func(test_name, help_text, required_args, expected_value)
# qui definiamo la funzione appena creata come metodo di classe
attrs[func.__name__] = func
return type.__new__(mcs, name, bases, attrs)
[documenti] def base_url_test(self, profile, action, url_name, args=None, data=None, return_code=None):
"""funzione veramente eseguita come test. Verrà tipicamente usata come mostrato
nell'esempio per permettere la corretta autenticazione
:param profile: il profilo (per auth, disponibile solo per override del metodo): owner,
customer...
:param action: una delle azioni elencate nella intestazione della classe
:param url_name: il nome utilizzato dalla reverse
:param args: args per la reverse
:param data: dati per create/update
:param return_code: il codice http atteso
"""
try:
endpoint = reverse(url_name, args=args)
response = self.client.get(endpoint, format='json')
except NoReverseMatch:
self.assertTrue(return_code == self.NOT_FOUND)
if action == "fail":
self.fail('endpoint to delete')
return
if action == "length":
# return_code: elements per page
# FORBIDDEN FIXME return_code è errato, non va confrontato con len...
self.assertEqual(len(response.json()['results']), return_code)
if action in ('list', 'retrieve'):
# SUCCESS // FORBIDDEN
response = self.client.get(endpoint, format='json')
if action == "create":
# CREATED // FORBIDDEN
response = self.client.post(endpoint, data, format='json')
if action == "destroy":
# NO_CONTENT // FORBIDDEN
response = self.client.delete(endpoint, format='json')
self.assertEqual(response.status_code, return_code)
return
if action == ('update', 'enable', 'disable'):
# SUCCESS // FORBIDDEN
response = self.client.patch(endpoint, data, format='json')
if action == ('status_set', 'status_reset'):
# SUCCESS // FORBIDDEN
data = {'status': 1 if 'action' == 'status_set' else 0}
response = self.client.patch(endpoint, data, format='json')
self.assertEqual(response.status_code, return_code)
self.assertEqual(response['content-type'], 'application/json')
[documenti] @classmethod
def get_test_from_dict(mcs):
"""method (generatore) che assembla gli argomenti per il test partendo dal dizionario
:param data_dict: dizionario con le info
:returns: una tupla name, help, (profile, action, url_name), return_code
:rtype: tuple
"""
for item in mcs.test_dict:
yield (item['name'],
item['help'],
(item['profile'], item['action'], item['url_name'],
item.get('data', None), item.get('args', None)),
item['return_code'])
[documenti] @classmethod
def get_data_from_csv(mcs):
"""Get test data from csv file (generator). If :attr:`csv_file`
is present, this method is used to get test data
:param csv_file: csv file that holds the test data
:returns: a tuple with name, help, (profile, action, url_name, data args), return_code
"""
Row = namedtuple('Row', ",".join(mcs.csv_field_list))
with open(mcs.csv_file) as f:
csv_reader = csv.reader(f, delimiter=mcs.csv_delimiter)
for item in csv_reader:
# Elimina righe vuote o commentate
if item and '#' not in item[0]:
row = Row._make(item)
yield (row.name,
row.help,
(row.profile, row.action, row.url_name, row.data, row.args),
int(row.return_code))
[documenti]class TestUrlMultipartMeta(TestUrlBaseMeta):
"""Variante di TestFunctionBaseMeta idonea per upload che avvengano con
``multipart/form-data``
"""
[documenti] def base_test_url(self, profile, action, url_name, data=None, args=None, return_code=None):
"""funzione veramente eseguita come test. Verrà tipicamente usata come mostrato.
"""
try:
endpoint = reverse(url_name, args=args)
response = self.client.get(endpoint, format='json')
except NoReverseMatch:
self.assertTrue(return_code == self.NOT_FOUND)
if action == "create":
# CREATED // FORBIDDEN
response = self.client.post(endpoint, data, content='multipart/form-data')
self.assertEqual(response.status_code, return_code)
if action == "update":
# SUCCESS // FORBIDDEN
response = self.client.patch(endpoint, data, content='multipart/form-data')
self.assertEqual(response.status_code, return_code)
self.assertEqual(response['content-type'], 'application/json')
def get_data_from_csv(csv_file):
"""Get test data from csv file (generator). If :attr:`csv_file`
is present, this method is used to get test data
:param csv_file: csv file that holds the test data
:returns: a tuple with name, help, (profile, action, url_name, data args), return_code
"""
csv_field_list = ('name', 'profile', 'action', 'url_name', 'arg', 'return_code', 'help')
Row = namedtuple('Row', ",".join(csv_field_list))
with open(csv_file) as f:
csv_reader = csv.reader(f, delimiter=';')
#for item in map(Row._make, csv_reader):
for item in csv_reader:
if item and '#' not in item[0]:
yield Row._make(item)