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)