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)