Test delle unità locale per Python 2

Il test delle unità ti consente di verificare la qualità del codice dopo averlo scritto, ma puoi anche usare il test delle unità per migliorare il processo di sviluppo man mano che procedi. Invece di scrivere test una volta terminato lo sviluppo dell'applicazione, valuta la possibilità di scrivere i test mentre via. Questo ti aiuta a progettare unità di codice di piccole dimensioni, riutilizzabili e gestibili. Inoltre, semplifica il test completo e rapido del codice.

Quando esegui test delle unità locali, esegui test che rimangono all'interno delle tue di sviluppo senza coinvolgere componenti remoti. App Engine fornisce utilità di test che utilizzano implementazioni locali di datastore e altre applicazioni Engine. Ciò significa che l'uso di questi servizi da parte del tuo codice localmente, senza eseguire il deployment di codice in App Engine mediante gli stub di servizio.

Uno stub di servizio è un metodo che simula il comportamento del servizio. Per Ad esempio, lo stub del servizio datastore mostrato La scrittura di test Datastore e Memcache ti consente di testare il codice del datastore senza effettuare alcuna richiesta al datastore reale. Qualsiasi archiviata durante il test delle unità del datastore è conservata in memoria, non e viene eliminato dopo l'esecuzione del test. Puoi eseguire piccoli test rapidi senza alcuna dipendenza dal datastore stesso.

Questo documento descrive come scrivere i test delle unità rispetto a diversi App Engine locali e fornisce alcune informazioni sulla configurazione di un framework di test.

Introduzione alle utilità di test di Python 2

un modulo Python di App Engine chiamato testbed rende disponibili gli stub di servizio per i test delle unità.

Gli stub di servizio sono disponibili per i seguenti servizi:

  • Identità app init_app_identity_stub
  • Archivio BLOB (utilizza init_blobstore_stub)
  • Capacità (utilizza init_capability_stub)
  • Datastore (utilizza init_datastore_v3_stub)
  • File (utilizza init_files_stub)
  • Immagini (solo per dev_appserver; utilizza init_images_stub)
  • LogService (utilizza init_logservice_stub)
  • Posta (utilizza init_mail_stub)
  • Memcache (utilizza init_memcache_stub)
  • Coda di attività (utilizza init_taskqueue_stub)
  • Recupero URL (utilizza init_urlfetch_stub)
  • Servizio utenti (utilizza init_user_stub)

Per inizializzare tutti gli stub contemporaneamente, puoi utilizzare init_all_stubs.

Scrittura di test Datastore e memcache

Questa sezione mostra un esempio di come scrivere codice che verifichi l'utilizzo della classe datastore e memcache.

Assicurati che l'esecutore del test disponga delle librerie appropriate sul caricamento Python incluse le librerie di App Engine, yaml (incluse in App Engine) SDK), la directory principale dell'applicazione ed eventuali altre modifiche al percorso della libreria previsto dal codice dell'applicazione (ad esempio una directory ./lib locale, se disponi uno). Ad esempio:

import sys
sys.path.insert(1, 'google-cloud-sdk/platform/google_appengine')
sys.path.insert(1, 'google-cloud-sdk/platform/google_appengine/lib/yaml/lib')
sys.path.insert(1, 'myapp/lib')

Importa il modulo unittest Python e i moduli App Engine pertinenti ai servizi in fase di test, in questo caso memcache e ndb, che utilizzano entrambi datastore e memcache. Importa anche testbed in maggior dettaglio più avanti in questo modulo.

import unittest

from google.appengine.api import memcache
from google.appengine.ext import ndb
from google.appengine.ext import testbed

Quindi, crea un corso TestModel. In questo esempio, una funzione controlla per vedere se un'entità è archiviata in memcache. Se non viene trovata alcuna entità, controlla se è presente nel datastore. Questo aspetto può spesso essere ridondante nella vita reale, poiché ndb usa memcache dietro le tende, ma è comunque un modello OK per test.

class TestModel(ndb.Model):
    """A model class used for testing."""
    number = ndb.IntegerProperty(default=42)
    text = ndb.StringProperty()


class TestEntityGroupRoot(ndb.Model):
    """Entity group root"""
    pass


def GetEntityViaMemcache(entity_key):
    """Get entity from memcache if available, from datastore if not."""
    entity = memcache.get(entity_key)
    if entity is not None:
        return entity
    key = ndb.Key(urlsafe=entity_key)
    entity = key.get()
    if entity is not None:
        memcache.set(entity_key, entity)
    return entity

Ora crea uno scenario di test. A prescindere dai servizi che stai testando, lo scenario di test devi creare un'istanza Testbed e attivarla. Lo scenario di test deve inoltre inizializzare gli stub di servizio pertinenti, in questo caso usando init_datastore_v3_stub e init_memcache_stub. I metodi per l'inizializzazione gli altri stub di servizio App Engine sono elencati in Introduzione al test Python Utilità.

class DatastoreTestCase(unittest.TestCase):

    def setUp(self):
        # First, create an instance of the Testbed class.
        self.testbed = testbed.Testbed()
        # Then activate the testbed, which prepares the service stubs for use.
        self.testbed.activate()
        # Next, declare which service stubs you want to use.
        self.testbed.init_datastore_v3_stub()
        self.testbed.init_memcache_stub()
        # Clear ndb's in-context cache between tests.
        # This prevents data from leaking between tests.
        # Alternatively, you could disable caching by
        # using ndb.get_context().set_cache_policy(False)
        ndb.get_context().clear_cache()

Il metodo init_datastore_v3_stub() senza argomento utilizza un metodo in memoria un datastore inizialmente vuoto. Se vuoi testare un datastore esistente , includi il suo percorso come argomento a init_datastore_v3_stub().

Oltre a setUp(), includi un metodo tearDown() che disattivi il parametro testato. Questa operazione ripristina gli stub originali in modo che i test non interferiscano con tra loro.

def tearDown(self):
    self.testbed.deactivate()

Poi implementa i test.

def testInsertEntity(self):
    TestModel().put()
    self.assertEqual(1, len(TestModel.query().fetch(2)))

Ora puoi utilizzare TestModel per scrivere test che utilizzano datastore o memcache stub di servizio invece di usare i servizi reali.

Ad esempio, il metodo mostrato di seguito crea due entità: la prima entità utilizza il valore predefinito per l'attributo number (42), mentre il secondo utilizza un valore non predefinito di number (17). Il metodo crea quindi una query TestModel, ma solo per quelle con il valore predefinito di number.

Dopo aver recuperato tutte le entità corrispondenti, il metodo verifica che esattamente un'entità e che il valore dell'attributo number di questa entità è il valore predefinito valore.

def testFilterByNumber(self):
    root = TestEntityGroupRoot(id="root")
    TestModel(parent=root.key).put()
    TestModel(number=17, parent=root.key).put()
    query = TestModel.query(ancestor=root.key).filter(
        TestModel.number == 42)
    results = query.fetch(2)
    self.assertEqual(1, len(results))
    self.assertEqual(42, results[0].number)

Come altro esempio, il metodo seguente crea un'entità e la recupera utilizzando la funzione GetEntityViaMemcache() creata sopra. Il metodo quindi verifica che un'entità sia stata restituita e che il suo valore number sia lo stesso per l'entità creata in precedenza.

def testGetEntityViaMemcache(self):
    entity_key = TestModel(number=18).put().urlsafe()
    retrieved_entity = GetEntityViaMemcache(entity_key)
    self.assertNotEqual(None, retrieved_entity)
    self.assertEqual(18, retrieved_entity.number)

Infine, richiama unittest.main().

if __name__ == '__main__':
    unittest.main()

Per eseguire i test, vedi Esecuzione dei test.

Scrittura dei test di Cloud Datastore

Se la tua app utilizza Cloud Datastore, potresti voler scrivere che verificano il comportamento della tua applicazione a fronte di eventuali coerenza. db.testbed mostra le opzioni che rendono questa facile:

from google.appengine.datastore import datastore_stub_util  # noqa


class HighReplicationTestCaseOne(unittest.TestCase):

    def setUp(self):
        # First, create an instance of the Testbed class.
        self.testbed = testbed.Testbed()
        # Then activate the testbed, which prepares the service stubs for use.
        self.testbed.activate()
        # Create a consistency policy that will simulate the High Replication
        # consistency model.
        self.policy = datastore_stub_util.PseudoRandomHRConsistencyPolicy(
            probability=0)
        # Initialize the datastore stub with this policy.
        self.testbed.init_datastore_v3_stub(consistency_policy=self.policy)
        # Initialize memcache stub too, since ndb also uses memcache
        self.testbed.init_memcache_stub()
        # Clear in-context cache before each test.
        ndb.get_context().clear_cache()

    def tearDown(self):
        self.testbed.deactivate()

    def testEventuallyConsistentGlobalQueryResult(self):
        class TestModel(ndb.Model):
            pass

        user_key = ndb.Key('User', 'ryan')

        # Put two entities
        ndb.put_multi([
            TestModel(parent=user_key),
            TestModel(parent=user_key)
        ])

        # Global query doesn't see the data.
        self.assertEqual(0, TestModel.query().count(3))
        # Ancestor query does see the data.
        self.assertEqual(2, TestModel.query(ancestor=user_key).count(3))

La classe PseudoRandomHRConsistencyPolicy ti consente di controllare la probabilità di un e scrivere l'applicazione prima di ogni query globale (non predecessore). Impostando il parametro probabilità pari allo 0%, stiamo istruendo lo stub del datastore in modo che operi la massima coerenza finale. Per la massima coerenza finale si intende esegue il commit delle scritture, ma la loro applicazione non riesce sempre, pertanto le query globali (non predecessori) non rileverà costantemente le modifiche. Questo non è ovviamente rappresentativo la quantità di coerenza finale che l'applicazione vedrà durante l'esecuzione ma a scopo di test, è molto utile poter configurare che il datastore locale si comporti ogni volta in questo modo. Se utilizzi un valore diverso da zero probabilità, PseudoRandomHRConsistencyPolicy crea una sequenza deterministica di decisioni in materia di coerenza in modo che i risultati dei test siano coerenti:

def testDeterministicOutcome(self):
    # 50% chance to apply.
    self.policy.SetProbability(.5)
    # Use the pseudo random sequence derived from seed=2.
    self.policy.SetSeed(2)

    class TestModel(ndb.Model):
        pass

    TestModel().put()

    self.assertEqual(0, TestModel.query().count(3))
    self.assertEqual(0, TestModel.query().count(3))
    # Will always be applied before the third query.
    self.assertEqual(1, TestModel.query().count(3))

Le API di test sono utili per verificare che l'applicazione funzioni correttamente a dispetto della coerenza finale, ma ricorda che l'Alta definizione locale Il modello di coerenza della lettura della replica è un'approssimazione del modello Modello a coerenza di lettura della replica, non una replica esatta. Nella regione eseguendo un get() di Entity che appartiene a un gruppo di entità con una scrittura non applicata, genererà sempre i risultati della scrittura non applicata visibile alle successive query globali. In fase di produzione non è così.

Redazione dei test di posta

Puoi utilizzare lo stub del servizio di posta per testare il servizio mail. Analogamente ad altri servizi supportati da testbed, all'inizio si inizializza lo stub, si richiami il codice che utilizza l'API mail e infine si verifica se sono stati inviati i messaggi corretti.

import unittest

from google.appengine.api import mail
from google.appengine.ext import testbed


class MailTestCase(unittest.TestCase):

    def setUp(self):
        self.testbed = testbed.Testbed()
        self.testbed.activate()
        self.testbed.init_mail_stub()
        self.mail_stub = self.testbed.get_stub(testbed.MAIL_SERVICE_NAME)

    def tearDown(self):
        self.testbed.deactivate()

    def testMailSent(self):
        mail.send_mail(to='alice@example.com',
                       subject='This is a test',
                       sender='bob@example.com',
                       body='This is a test e-mail')
        messages = self.mail_stub.get_sent_messages(to='alice@example.com')
        self.assertEqual(1, len(messages))
        self.assertEqual('alice@example.com', messages[0].to)

Scrittura dei test delle coda di attività corso...

Puoi usare lo stub della coda di attività per scrivere test che utilizzano taskqueue. Simile ad altri supportati da testbed, inizialmente si inizializza lo stub, quindi si richiama il che utilizza l'API Taskqueue, nonché per verificare se le attività sono state correttamente aggiunto alla coda.

import operator
import os
import unittest

from google.appengine.api import taskqueue
from google.appengine.ext import deferred
from google.appengine.ext import testbed


class TaskQueueTestCase(unittest.TestCase):
    def setUp(self):
        self.testbed = testbed.Testbed()
        self.testbed.activate()

        # root_path must be set the the location of queue.yaml.
        # Otherwise, only the 'default' queue will be available.
        self.testbed.init_taskqueue_stub(
            root_path=os.path.join(os.path.dirname(__file__), 'resources'))
        self.taskqueue_stub = self.testbed.get_stub(
            testbed.TASKQUEUE_SERVICE_NAME)

    def tearDown(self):
        self.testbed.deactivate()

    def testTaskAddedToQueue(self):
        taskqueue.Task(name='my_task', url='/url/of/my/task/').add()
        tasks = self.taskqueue_stub.get_filtered_tasks()
        self.assertEqual(len(tasks), 1)
        self.assertEqual(tasks[0].name, 'my_task')

Impostazione del file di configurazione queue.yaml

Se vuoi eseguire test su codice che interagisce con una coda non predefinita, devi creare e specificare un file queue.yaml che l'applicazione possa utilizzare. Di seguito è riportato un esempio di queue.yaml:

Per ulteriori informazioni sulle opzioni della coda.yaml disponibili, consulta la sezione Coda di attività configurazione.

queue:
- name: default
  rate: 5/s
- name: queue-1
  rate: 5/s
- name: queue-2
  rate: 5/s

La posizione di queue.yaml viene specificata durante l'inizializzazione dello stub:

self.testbed.init_taskqueue_stub(root_path='.')

Nell'esempio, queue.yaml si trova nella stessa directory dei test. Se fosse presente in un'altra cartella, quel percorso deve essere specificato in root_path.

Applicazione di filtri alle attività

Lo stub della coda di attività get_filtered_tasks consente di filtrare le attività in coda. Ciò semplifica la scrittura dei test che devono verificare il codice che accoda più attività.

def testFiltering(self):
    taskqueue.Task(name='task_one', url='/url/of/task/1/').add('queue-1')
    taskqueue.Task(name='task_two', url='/url/of/task/2/').add('queue-2')

    # All tasks
    tasks = self.taskqueue_stub.get_filtered_tasks()
    self.assertEqual(len(tasks), 2)

    # Filter by name
    tasks = self.taskqueue_stub.get_filtered_tasks(name='task_one')
    self.assertEqual(len(tasks), 1)
    self.assertEqual(tasks[0].name, 'task_one')

    # Filter by URL
    tasks = self.taskqueue_stub.get_filtered_tasks(url='/url/of/task/1/')
    self.assertEqual(len(tasks), 1)
    self.assertEqual(tasks[0].name, 'task_one')

    # Filter by queue
    tasks = self.taskqueue_stub.get_filtered_tasks(queue_names='queue-1')
    self.assertEqual(len(tasks), 1)
    self.assertEqual(tasks[0].name, 'task_one')

    # Multiple queues
    tasks = self.taskqueue_stub.get_filtered_tasks(
        queue_names=['queue-1', 'queue-2'])
    self.assertEqual(len(tasks), 2)

Scrittura di test di attività differite

Se il codice dell'applicazione utilizza la libreria differita, puoi utilizzare la coda di attività stub insieme a deferred per verificare che le funzioni differite siano in coda eseguito correttamente.

def testTaskAddedByDeferred(self):
    deferred.defer(operator.add, 1, 2)

    tasks = self.taskqueue_stub.get_filtered_tasks()
    self.assertEqual(len(tasks), 1)

    result = deferred.run(tasks[0].payload)
    self.assertEqual(result, 3)

Modifica delle variabili di ambiente predefinite

I servizi App Engine spesso dipendono dalle variabili di ambiente. Il metodo activate() della classe testbed.Testbed utilizza valori predefiniti, ma puoi impostare valori personalizzati in base alle tue esigenze di test con setup_env della classe testbed.Testbed.

Ad esempio, supponiamo di avere un test che memorizza diverse entità di Cloud Shell, tutti collegati allo stesso ID applicazione. Ora vuoi eseguire gli stessi test, ma usando un ID applicazione diverso da quello collegate alle entità archiviate. Per farlo, passa il nuovo valore self.setup_env() sotto forma di app_id.

Ad esempio:

import os
import unittest

from google.appengine.ext import testbed


class EnvVarsTestCase(unittest.TestCase):
    def setUp(self):
        self.testbed = testbed.Testbed()
        self.testbed.activate()
        self.testbed.setup_env(
            app_id='your-app-id',
            my_config_setting='example',
            overwrite=True)

    def tearDown(self):
        self.testbed.deactivate()

    def testEnvVars(self):
        self.assertEqual(os.environ['APPLICATION_ID'], 'your-app-id')
        self.assertEqual(os.environ['MY_CONFIG_SETTING'], 'example')

Accesso simulato

Un altro uso frequente di setup_env è simulare l'accesso di un utente, con o senza privilegi amministrativi, per verificare se i tuoi gestori operano correttamente in ogni caso.

import unittest

from google.appengine.api import users
from google.appengine.ext import testbed


class LoginTestCase(unittest.TestCase):
    def setUp(self):
        self.testbed = testbed.Testbed()
        self.testbed.activate()
        self.testbed.init_user_stub()

    def tearDown(self):
        self.testbed.deactivate()

    def loginUser(self, email='user@example.com', id='123', is_admin=False):
        self.testbed.setup_env(
            user_email=email,
            user_id=id,
            user_is_admin='1' if is_admin else '0',
            overwrite=True)

    def testLogin(self):
        self.assertFalse(users.get_current_user())
        self.loginUser()
        self.assertEquals(users.get_current_user().email(), 'user@example.com')
        self.loginUser(is_admin=True)
        self.assertTrue(users.is_current_user_admin())

Ora i tuoi metodi di test possono chiamare, ad esempio, self.loginUser('', '') per simulare l'assenza di accessi di un utente, self.loginUser('test@example.com', '123') a simulare l'accesso di un utente non amministratore, self.loginUser('test@example.com', '123', is_admin=True) per simulare l'accesso di un utente amministratore.

Configurazione di un framework di test

Le utilità di test dell'SDK non sono legate a un framework specifico. Puoi eseguire i test delle unità con qualsiasi testrunner di App Engine disponibile, ad esempio nose- gae o ferrisnose. Puoi anche scrivere un testrunner di prova oppure usa quello mostrato di seguito.

I seguenti script utilizzano la tecnica Python unittest.

Puoi assegnare allo script il nome che preferisci. Quando lo esegui, fornisci il percorso l'installazione di Google Cloud CLI o dell'SDK di Google App Engine e il percorso moduli di test. Lo script rileverà tutti i test nel percorso fornito e i risultati verranno stampati nel flusso di errori standard. I file di test seguono la convenzione di con test aggiunto come prefisso al nome.

"""App Engine local test runner example.

This program handles properly importing the App Engine SDK so that test modules
can use google.appengine.* APIs and the Google App Engine testbed.

Example invocation:

    $ python runner.py ~/google-cloud-sdk
"""

import argparse
import os
import sys
import unittest


def fixup_paths(path):
    """Adds GAE SDK path to system path and appends it to the google path
    if that already exists."""
    # Not all Google packages are inside namespace packages, which means
    # there might be another non-namespace package named `google` already on
    # the path and simply appending the App Engine SDK to the path will not
    # work since the other package will get discovered and used first.
    # This emulates namespace packages by first searching if a `google` package
    # exists by importing it, and if so appending to its module search path.
    try:
        import google
        google.__path__.append("{0}/google".format(path))
    except ImportError:
        pass

    sys.path.insert(0, path)


def main(sdk_path, test_path, test_pattern):
    # If the SDK path points to a Google Cloud SDK installation
    # then we should alter it to point to the GAE platform location.
    if os.path.exists(os.path.join(sdk_path, 'platform/google_appengine')):
        sdk_path = os.path.join(sdk_path, 'platform/google_appengine')

    # Make sure google.appengine.* modules are importable.
    fixup_paths(sdk_path)

    # Make sure all bundled third-party packages are available.
    import dev_appserver
    dev_appserver.fix_sys_path()

    # Loading appengine_config from the current project ensures that any
    # changes to configuration there are available to all tests (e.g.
    # sys.path modifications, namespaces, etc.)
    try:
        import appengine_config
        (appengine_config)
    except ImportError:
        print('Note: unable to import appengine_config.')

    # Discover and run tests.
    suite = unittest.loader.TestLoader().discover(test_path, test_pattern)
    return unittest.TextTestRunner(verbosity=2).run(suite)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument(
        'sdk_path',
        help='The path to the Google App Engine SDK or the Google Cloud SDK.')
    parser.add_argument(
        '--test-path',
        help='The path to look for tests, defaults to the current directory.',
        default=os.getcwd())
    parser.add_argument(
        '--test-pattern',
        help='The file pattern for test modules, defaults to *_test.py.',
        default='*_test.py')

    args = parser.parse_args()

    result = main(args.sdk_path, args.test_path, args.test_pattern)

    if not result.wasSuccessful():
        sys.exit(1)

Esecuzione dei test

Puoi eseguire questi test semplicemente eseguendo lo script runner.py, che descritto in dettaglio in Configurare un framework di test:

python runner.py <path-to-appengine-or-gcloud-SDK> .