Testing notifications¶
When your application calls send_notification() with a context dict, the template
expects certain keys to be present with certain types. A mismatch – a missing key or
a wrong type – only surfaces at notification time, possibly in production.
Tattler lets you declare the expected context for each event in a context.json file
and validate your code against it in your test suite. This same context.json is also
used for live previews.
Declaring the expected context¶
Place a context.json file inside the event template directory:
templates/
└── mywebapp/ # scope
└── order_confirmation/ # event
├── context.json # expected context declaration
└── email/
├── body.txt
└── subject.txt
The file contains a JSON object whose keys and value types mirror what your template expects:
{
"order": {
"number": "ORD-1234",
"total": 49.90,
"placed_at": "^tattler^datetime^2024-06-28T18:15:04"
},
"customer_name": "Jane Doe"
}
The actual values are irrelevant – only keys and types matter. For example,
"number": "ORD-1234" declares that order.number must be a string, and
"total": 49.90 declares it must be a float.
For temporal types, use tattler’s type markers
(e.g. ^tattler^datetime^...) so the declaration carries the correct Python type.
A null value means “any type is accepted” – useful for optional or polymorphic fields.
Validating context in tests¶
Tattler provides assert_context_complete(),
a helper designed for use in unittest test cases. It checks that a context dict:
Is serializable (can survive the client-to-server wire format).
Contains all keys declared in
context.json.Has values whose types match those declared.
Say your application has a function that builds a context and sends a notification:
# myapp/notifications.py
from datetime import datetime
from tattler.client.tattler_py import send_notification
def notify_order_confirmed(customer, order):
"""Send an order-confirmation notification."""
...
context = {
'order': order, # send_notification() serializes these objects into JSON
'customer': customer,
}
send_notification('mywebapp', 'order_confirmation', customer.email, context=context)
To validate the context, mock send_notification() and pass the captured context
to assert_context_complete():
# tests/test_notifications.py
import unittest
from pathlib import Path
from unittest import mock
from tattler.utils.context_validation import assert_context_complete
from myapp.notifications import notify_order_confirmed
TEMPLATES_ROOT = Path('/path/to/templates')
class TestOrderNotifications(unittest.TestCase):
def test_order_confirmation_context(self, mock_send):
"""Verify the context we pass matches what the template expects."""
with mock.patch('myapp.notifications.send_notification') as mock_send:
# have the code call send_notification()
notify_order_confirmed(order, customer)
# validate the context it provided to send_notification()
_, kwargs = mock_send.call_args
context = kwargs['context']
assert_context_complete(self, 'mywebapp', 'order_confirmation', context, TEMPLATES_ROOT)
If the context is missing a key, has a wrong type, or is not serializable, the test fails with a clear message listing every violation:
mywebapp/order_confirmation: context validation failed:
'.order.placed_at': expected datetime, got str
'.customer_name': missing key
What gets checked¶
Missing keys – every key in
context.jsonmust be present in the actual context.Type mismatches – the Python type of each value must match (
int,str,float,datetime, etc.).Nested dicts – validated recursively, with dotted paths in error messages (e.g.
order.total).Lists of dicts – each element is validated against the first element declared in
context.json.Lists of scalars – only the list type itself is checked, not individual element types.
Serializability – ORM objects, model instances and other non-JSON types are serialized first, and the test fails if serialization itself fails.
Extra keys – keys present in the actual context but absent from
context.jsonare accepted silently.Null declarations – a
nullvalue incontext.jsonaccepts any type in the actual context.Null values in context – by default,
Nonevalues in the actual context are accepted even whencontext.jsondeclares a non-null type. Passaccept_null=Falseto reject them.
Typical workflow¶
Your template designer creates or updates a template and its
context.json.You write a test that builds the context your code will pass to
send_notification()and callsassert_context_complete().When the template interface changes (keys added, types modified), the test fails, telling you exactly what to update in your code.
This keeps your notification-triggering code and your templates in sync, catching contract violations at test time rather than in production.
API reference¶
- tattler.utils.context_validation.assert_context_complete(test_case, scope, event, context, templates_base, accept_null=True)¶
Assert that context covers all keys and types from context.json.
First serializes context via
serialize_json()(asserting serializability), then deserializes both sides and compares structure.- Parameters:
test_case (
TestCase) – Theunittest.TestCaseinstance (for assertions).scope (
str) – Notification scope (e.g.'website').event (
str) – Event name (e.g.'order_confirmation').context (
Mapping[str,Any]) – The context dict passed tosend_notification().templates_base (
str|PathLike) – Path to the templates root directory.accept_null (
bool) – If True (default), allow None for any field.
- Raises:
AssertionError – If context is not serializable or misses keys/types.
- Return type:
None