======= Testing ======= .. contents:: :local: What do you need to test? ========================= With my applications, I need to verify the following things: 1. Is my application configured correctly? 2. Does Sentry send events when errors are kicked up in various critical parts of my application? 3. Are the Sentry events scrubbed correctly? 4. Does scrubbing Sentry events kick up errors? 5. Has the shape of the event that Sentry sends changed because of a change in integrations, sentry-sdk client upgrade, or something else? That results in a few different kinds of tests I run. * In local testing and CI: * Integration tests to verify that Sentry is configured correctly, scrubbing works, and the shape of the Sentry event hasn't changed since last time I examined it. * In production: * A way to trigger a Sentry event to verify that Sentry is configured correctly and events reach the Sentry server. Integration testing with SentryTestHelper ========================================= Fillmore provides a ``SentryTestHelper`` to make it convenient to create new or reuse existing Sentry clients in a way that overrides the transport allowing you to inspect and assert things against Sentry events that were emitted. The helper provides two ways to use it: * :py:func:`fillmore.test.SentryTestHelper.init` a new Sentry client with provided parameters * OR, :py:func:`fillmore.test.SentryTestHelper.reuse` an existing Sentry client that was configured in your application code Both of those create new contexts and clear the event list. You can create multiple contexts in a single test. Calling *init* or *reuse* returns an object that keeps track of what events were emitted and stores them as a list in the ``.events`` property. Here's an example test using ``unittest``: .. [[[cog import cog cog.outl("\n.. code-block:: python\n") with open("examples/myapp/myapp/test_app.py", "r") as fp: for line in fp: cog.out(f" {line}") cog.outl("") ]]] .. code-block:: python # examples/myapp/myapp/test_app.py import unittest from fillmore.test import SentryTestHelper from myapp.app import kick_up_exception class TestApp(unittest.TestCase): def test_scrubber(self): # Reuse the existing Sentry configuration and set up the helper # to capture Sentry events sentry_test_helper = SentryTestHelper() with sentry_test_helper.reuse() as sentry_client: kick_up_exception() (payload,) = sentry_client.envelope_payloads error = payload["exception"]["values"][0] self.assertEqual(error["type"], "Exception") self.assertEqual(error["value"], "internal exception") self.assertEqual( error["stacktrace"]["frames"][0]["vars"]["username"], "[Scrubbed]" ) .. [[[end]]] Fillmore also provides a `pytest `__ fixture. Here's an example test using pytest: .. [[[cog import cog cog.outl("\n.. code-block:: python\n") with open("examples/myapp_pytest/myapp/test_app.py", "r") as fp: for line in fp: cog.out(f" {line}") cog.outl("") ]]] .. code-block:: python # examples/myapp_pytest/myapp/test_app.py from myapp.app import kick_up_exception def test_scrubber(sentry_helper, caplog): # Reuse the existing Sentry configuration and set up the helper # to capture Sentry events with sentry_helper.reuse() as sentry_client: kick_up_exception() # Assert things against the Sentry event records (payload,) = sentry_client.envelope_payloads error = payload["exception"]["values"][0] assert error["type"] == "Exception" assert error["value"] == "internal exception" assert error["stacktrace"]["frames"][0]["vars"]["username"] == "[Scrubbed]" # Assert things against the logging messages created fillmore_records = [ rec for rec in caplog.record_tuples if rec[0].startswith("fillmore") ] assert len(fillmore_records) == 0 .. [[[end]]] Integration testing against Kent--a fakesentry service ====================================================== `Kent `__ is a service that you can run in CI or on your development machine which can accept Sentry event submissions and has an API to let you programmatically examine them. Because Kent is keeping the entire event payload, you know exactly what got sent and you can hone your scrubbing accordingly. This lets you write integration tests that run in CI in an environment that has multiple services. For example, if you set Kent up at ``http://public@localhost:8090/0`` and you had ``SENTRY_DSN`` set to that dsn, then you could access it like this: .. [[[cog import cog cog.outl("\n.. code-block:: python\n") with open("examples/testing/kent_testing.py", "r") as fp: for line in fp: cog.out(f" {line}") cog.outl("") ]]] .. code-block:: python # examples/testing/kent_testing.py import time from fillmore.test import get_sentry_base_url, SentryTestHelper import requests # Use the werkzeug wsgi client because the Django test client fakes # everything from werkzeug.test import Client from django.conf import settings from myapp.wsgi import application def test_sentry_with_kent(): sentry_helper = SentryTestHelper() client = Client(application) kent_api = get_sentry_base_url(settings.SENTRY_DSN) # Flush the events from Kent and assert there are 0 resp = requests.post(f"{kent_api}api/flush/") assert resp.status_code == 200 resp = requests.get(f"{kent_api}api/errorlist/") assert len(resp.json()["errors"]) == 0 # reuse uses an existing configured Sentry client, but mocks the # transport so you can assert things against the Sentry events # generated with sentry_helper.reuse(): resp = client.get("/broken") assert resp.status_code == 500 # Give sentry_sdk a chance to send the events to Kent time.sleep(1) # Get the event list and then the event itself resp = requests.get(f"{kent_api}api/errorlist/") event_data = resp.json()["errors"] assert len(event_data) == 1 error_id = event_data[0] resp = requests.get(f"{kent_api}api/error/{error_id}") event = resp.json()["payload"] # Assert things against the event assert "django" in event["sdk"]["integrations"] assert "request" in event assert event["request"]["headers"]["Auth-Token"] == "[Scrubbed]" # FIXME: Assert that Fillmore didn't log any exceptions .. [[[end]]] Check logging for errors ======================== If Fillmore raised an exception when scrubbing, it'll log a message to the ``fillmore`` logger. Your tests should assert that there are no messages at ``logging.ERROR`` level logged from the ``fillmore`` logger. Test module API =============== .. automodule:: fillmore.test :members: