Pengujian Unit Lokal untuk Python 2

Pengujian unit memungkinkan Anda memeriksa kualitas kode setelah menulisnya, tetapi Anda juga dapat menggunakan pengujian unit untuk meningkatkan proses pengembangan seiring progres Anda. Daripada menulis pengujian setelah Anda selesai mengembangkan aplikasi, sebaiknya tulis pengujian seiring progres Anda. Hal ini membantu Anda mendesain unit kode yang kecil, mudah dikelola, dan dapat digunakan kembali. Hal ini juga memudahkan Anda menguji kode secara menyeluruh dan cepat.

Saat melakukan pengujian unit lokal, Anda menjalankan pengujian yang tetap berada di dalam lingkungan pengembangan Anda sendiri tanpa melibatkan komponen jarak jauh. App Engine menyediakan utilitas pengujian yang menggunakan implementasi lokal datastore dan layanan App Engine lainnya. Ini berarti Anda dapat menerapkan penggunaan kode Anda atas layanan ini secara lokal, tanpa men-deploy kode ke App Engine, dengan menggunakan service stub.

Service stub adalah metode yang menyimulasikan perilaku layanan. Misalnya, service stub datastore yang ditampilkan dalam Menulis Pengujian Datastore dan Memcache memungkinkan Anda menguji kode datastore tanpa membuat permintaan ke datastore sebenarnya. Setiap entity yang disimpan selama pengujian unit datastore akan disimpan di memori, bukan di datastore, dan dihapus setelah pengujian dijalankan. Anda dapat menjalankan pengujian kecil dan cepat tanpa dependensi pada datastore itu sendiri.

Dokumen ini menjelaskan cara menulis pengujian unit pada beberapa layanan App Engine lokal, lalu memberikan sejumlah informasi tentang penyiapan framework pengujian.

Memperkenalkan aplikasi utilitas pengujian Python 2

Modul App Engine Python yang disebut testbed menyediakan stub layanan untuk pengujian unit.

Stub layanan tersedia untuk layanan berikut:

  • Identitas Aplikasi init_app_identity_stub
  • Blobstore (gunakan init_blobstore_stub)
  • Kemampuan (gunakan init_capability_stub)
  • Datastore (gunakan init_datastore_v3_stub)
  • File (gunakan init_files_stub)
  • Gambar (hanya untuk dev_appserver; gunakan init_images_stub)
  • LogService (gunakan init_logservice_stub)
  • Email (gunakan init_mail_stub)
  • Memcache (gunakan init_memcache_stub)
  • Antrean Tugas (gunakan init_taskqueue_stub)
  • Pengambilan URL (gunakan init_urlfetch_stub)
  • Layanan pengguna (gunakan init_user_stub)

Untuk melakukan inisialisasi semua stub secara bersamaan, Anda dapat menggunakan init_all_stubs.

Menulis pengujian Datastore dan memcache

Bagian ini menunjukkan contoh cara menulis kode yang menguji penggunaan layanan datastore dan memcache.

Pastikan runner pengujian Anda memiliki library yang sesuai di jalur pemuatan Python, termasuk library App Engine, yaml (disertakan dalam App Engine SDK), root aplikasi, dan modifikasi lainnya pada jalur library yang diharapkan oleh kode aplikasi (seperti direktori ./lib lokal, jika ada ). Contoh:

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')

Impor modul unittest Python dan modul App Engine yang relevan dengan layanan yang sedang diuji—dalam hal ini memcache dan ndb, yang menggunakan datastore dan memcache. Impor juga modul testbed.

import unittest

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

Lalu, buat class TestModel. Dalam contoh ini, fungsi melakukan pemeriksaan untuk melihat apakah entity disimpan dalam memcache. Jika tidak ada entity yang ditemukan, entity akan diperiksa di datastore. Hal ini sering kali berulang di kondisi nyata, karena ndb menggunakan memcache sendiri di belakang layar, tetapi ini masih merupakan pola yang bagus untuk pengujian.

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

Selanjutnya, buat kasus pengujian. Apa pun layanan yang Anda uji, kasus pengujian harus membuat instance Testbed dan mengaktifkannya. Kasus pengujian juga harus menginisialisasi stub layanan yang relevan. Dalam hal ini, menggunakan init_datastore_v3_stub dan init_memcache_stub. Metode untuk menginisialisasi stub layanan App Engine lainnya tercantum dalam Memperkenalkan Utilitas Pengujian Python.

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()

Metode init_datastore_v3_stub() tanpa argumen menggunakan datastore dalam memori yang awalnya kosong. Jika Anda ingin menguji entity datastore yang ada, sertakan nama jalurnya sebagai argumen untuk init_datastore_v3_stub().

Selain setUp(), sertakan metode tearDown() yang menonaktifkan testbed. Tindakan ini akan memulihkan stub asli sehingga pengujian tidak mengganggu satu sama lain.

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

Kemudian, terapkan pengujian.

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

Sekarang Anda dapat menggunakan TestModel untuk menulis pengujian yang menggunakan stub layanan datastore atau memcache, alih-alih menggunakan layanan sebenarnya.

Misalnya, metode yang ditampilkan di bawah membuat dua entity: entity pertama menggunakan nilai default untuk atribut number (42), dan entity kedua menggunakan nilai non-default untuk number (17). Metode tersebut kemudian membuat kueri untuk entity TestModel, tetapi hanya untuk entity dengan nilai default number.

Setelah mengambil semua entity yang cocok, metode ini akan menguji bahwa hanya satu entity yang ditemukan, dan bahwa nilai atribut number entity tersebut adalah nilai default.

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)

Sebagai contoh lainnya, metode berikut membuat entity dan mengambilnya menggunakan fungsi GetEntityViaMemcache() yang kita buat di atas. Metode ini kemudian menguji bahwa entity ditampilkan, dan bahwa nilai number-nya sama dengan entity yang dibuat sebelumnya.

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)

Terakhir, panggil unittest.main().

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

Untuk menjalankan pengujian, lihat Menjalankan pengujian.

Menulis pengujian Cloud Datastore

Jika aplikasi Anda menggunakan Cloud Datastore, Anda mungkin ingin menulis pengujian yang memverifikasi perilaku aplikasi dalam menghadapi konsistensi tertunda. db.testbed menampilkan opsi yang mempermudah hal ini:

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))

Class PseudoRandomHRConsistencyPolicy memungkinkan Anda mengontrol kemungkinan penerapan penulisan sebelum setiap kueri global (non-ancestor). Dengan menetapkan probabilitas ke 0%, kami menginstruksikan stub datastore agar beroperasi dengan jumlah konsistensi maksimum. Konsistensi akhir maksimum berarti penulisan akan di-commit tetapi selalu gagal diterapkan, sehingga kueri global (non-ancestor) akan terus gagal melihat perubahan. Hal ini tentu saja tidak mewakili jumlah konsistensi akhir yang akan dilihat aplikasi Anda saat berjalan dalam produksi, tetapi untuk tujuan pengujian, sangat berguna untuk dapat mengonfigurasi datastore lokal agar berperilaku seperti ini setiap saat. Jika Anda menggunakan probabilitas bukan nol, PseudoRandomHRConsistencyPolicy akan membuat urutan keputusan konsistensi yang deterministik sehingga hasil pengujiannya konsisten:

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))

API pengujian berguna untuk memverifikasi bahwa aplikasi Anda berperilaku dengan baik dalam menghadapi konsistensi tertunda, tetapi perlu diingat bahwa model konsistensi pembacaan Replikasi Tinggi lokal adalah perkiraan dari model konsistensi pembacaan Replikasi Tinggi produksi, bukan replika yang sama persis. Di lingkungan lokal, menjalankan get() dari Entity yang termasuk dalam grup entity dengan penulisan yang belum diterapkan akan selalu membuat hasil penulisan yang belum diterapkan terlihat oleh kueri global berikutnya. Dalam produksi, hal ini tidak terjadi.

Menulis pengujian email

Anda dapat menggunakan stub layanan email untuk menguji layanan email. Serupa dengan layanan lain yang didukung oleh testbed, pertama-tama Anda melakukan inisialisasi stub, lalu memanggil kode yang menggunakan mail API, dan akhirnya menguji apakah pesan yang benar telah dikirim.

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)

Menulis pengujian task queue

Anda dapat menggunakan stub taskqueue untuk menulis pengujian yang menggunakan layanan taskqueue. Serupa dengan layanan lain yang didukung oleh testbed, pertama-tama lakukanlah inisialisasi stub, lalu panggil kode yang menggunakan taskqueue API, dan terakhir uji apakah tugas ditambahkan dengan benar ke antrean.

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')

Menyetel file konfigurasi queue.yaml

Jika ingin menjalankan pengujian pada kode yang berinteraksi dengan antrean non-default, Anda harus membuat dan menentukan file queue.yaml yang akan digunakan aplikasi Anda. Berikut inilah contoh queue.yaml:

Untuk mengetahui informasi selengkapnya tentang opsi queue.yaml yang tersedia, lihat konfigurasi task queue.

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

Lokasi queue.yaml ditentukan saat melakukan inisialisasi stub:

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

Dalam contoh ini, queue.yaml berada di direktori yang sama dengan pengujian. Jika ada di folder lain, jalur tersebut harus ditentukan di root_path.

Memfilter tugas

get_filtered_tasks stub tugas memungkinkan Anda memfilter tugas yang diantrekan. Hal ini mempermudah penulisan pengujian yang perlu memverifikasi kode yang mengantrekan beberapa tugas.

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)

Menulis pengujian tugas yang ditangguhkan

Jika kode aplikasi Anda menggunakan library yang ditangguhkan, Anda dapat menggunakan stub taskqueue bersama dengan deferred untuk memverifikasi bahwa fungsi yang ditangguhkan telah dimasukkan ke dalam antrean dan dieksekusi dengan benar.

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)

Mengubah variabel lingkungan default

Layanan App Engine sering kali bergantung pada variabel lingkungan. Metode activate() class testbed.Testbed menggunakan nilai default, tetapi Anda dapat menetapkan nilai kustom berdasarkan kebutuhan pengujian dengan metode setup_env class testbed.Testbed.

Misalnya, Anda memiliki pengujian yang menyimpan beberapa entity di datastore, semuanya ditautkan ke ID aplikasi yang sama. Sekarang Anda ingin menjalankan pengujian yang sama lagi, tetapi menggunakan ID aplikasi yang berbeda dari ID aplikasi yang ditautkan ke entity tersimpan. Untuk melakukannya, teruskan nilai baru ke self.setup_env() sebagai app_id.

Contoh:

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')

Menyimulasikan login

Penggunaan sering lainnya untuk setup_env adalah untuk menyimulasikan pengguna yang sedang login, baik dengan atau tanpa hak istimewa admin, untuk memeriksa apakah pengendali Anda beroperasi dengan benar dalam setiap kasus.

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())

Sekarang, metode pengujian Anda dapat memanggil, misalnya, self.loginUser('', '') untuk menyimulasikan tidak ada pengguna yang login, self.loginUser('test@example.com', '123') untuk menyimulasikan pengguna non-admin yang login, self.loginUser('test@example.com', '123', is_admin=True) untuk menyimulasikan pengguna admin yang sedang login.

Menyiapkan framework pengujian

Aplikasi utilitas pengujian SDK tidak terikat dengan framework tertentu. Anda dapat menjalankan pengujian unit dengan testrunner App Engine yang tersedia, misalnya nose- gae atau ferrisnose. Anda juga dapat menulis testrunner sederhana sendiri, atau menggunakan testrunner yang ditampilkan di bawah ini.

Skrip berikut menggunakan modul unittest Python.

Anda dapat memberi nama skrip tersebut sesuai keinginan. Saat Anda menjalankannya, berikan jalur ke penginstalan Google Cloud CLI atau Google App Engine SDK dan jalur ke modul pengujian Anda. Skrip akan menemukan semua pengujian di jalur yang disediakan dan akan mencetak hasilnya ke aliran data error standar. File pengujian yang mengikuti konvensi memiliki test yang diawali dengan namanya.

"""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)

Menjalankan pengujian

Anda dapat menjalankan pengujian ini hanya dengan menjalankan skrip runner.py, yang dijelaskan secara mendetail di bagian Menyiapkan framework pengujian:

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