OneSignal Push Notification Testing: API Testing, Segment Targeting, and A/B Test Notifications
OneSignal abstracts the complexity of FCM and APNS behind a unified REST API, but that means your integration is only as reliable as your HTTP layer. This guide covers testing OneSignal notification delivery, segment targeting, A/B test setup, and delivery reporting using Python with requests-mock and httpretty — all without hitting real OneSignal endpoints.
Key Takeaways
OneSignal's REST API distinguishes between player IDs, external user IDs, and segments — test all three targeting modes. Using the wrong targeting parameter silently sends to the wrong audience or returns a 400 error; unit tests catch this before it hits production.
The notification ID in the create response is your tracking handle — always assert it's stored. Your service must persist the OneSignal notification ID returned from the create call to later query delivery status; tests that skip this assertion miss a common integration gap.
A/B test notifications use variants with weight — test that weights sum to 100. OneSignal rejects A/B payloads where variant weights don't sum correctly; a validation test prevents silent failures in your notification pipeline.
Delivery reporting requires a separate GET call — test the polling logic. The notifications/{id} endpoint returns successful, failed, errored, converted counts; your analytics pipeline should poll this and your tests must cover the polling logic.
requests-mock is preferable over mocking the requests library directly. requests-mock provides URL-pattern matching, request body inspection, and response queueing that makes complex multi-call scenarios (create then query) easy to express in tests.
OneSignal is the most widely used push notification platform for mobile and web, used by tens of thousands of apps. Despite this, most teams test their OneSignal integration with... a manual phone test. This guide replaces that with a robust unit test suite that covers every integration point your server code touches.
The OneSignal API Surface You Need to Test
Your server typically calls three OneSignal API groups:
- Notifications API — create and query notifications
- Players API — manage device registrations and external user IDs
- Apps API — read app-level stats
We'll focus on the Notifications API since that's where business logic lives.
Setting Up the Test Environment
pip install requests requests-mock httpretty pytest pytest-mockBuild a thin OneSignal client your tests can exercise:
# myapp/onesignal_client.py
import requests
ONESIGNAL_API_BASE = "https://onesignal.com/api/v1"
class OneSignalClient:
def __init__(self, app_id: str, api_key: str):
self.app_id = app_id
self.api_key = api_key
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Basic {api_key}",
"Content-Type": "application/json",
})
def create_notification(self, payload: dict) -> dict:
payload["app_id"] = self.app_id
response = self.session.post(
f"{ONESIGNAL_API_BASE}/notifications",
json=payload
)
response.raise_for_status()
return response.json()
def get_notification(self, notification_id: str) -> dict:
response = self.session.get(
f"{ONESIGNAL_API_BASE}/notifications/{notification_id}",
params={"app_id": self.app_id}
)
response.raise_for_status()
return response.json()
def cancel_notification(self, notification_id: str) -> dict:
response = self.session.delete(
f"{ONESIGNAL_API_BASE}/notifications/{notification_id}",
params={"app_id": self.app_id}
)
response.raise_for_status()
return response.json()Testing Notification Creation by Player IDs
# test_onesignal_player_targeting.py
import requests_mock as requests_mock_lib
import pytest
import json
from myapp.onesignal_client import OneSignalClient
from myapp.push_service import OneSignalPushService
APP_ID = "test-app-id-1234"
API_KEY = "test-api-key-5678"
NOTIFICATION_URL = "https://onesignal.com/api/v1/notifications"
@pytest.fixture
def client():
return OneSignalClient(app_id=APP_ID, api_key=API_KEY)
@pytest.fixture
def service():
return OneSignalPushService(app_id=APP_ID, api_key=API_KEY)
def test_send_to_player_ids_builds_correct_payload(client):
player_ids = ["player-abc", "player-def", "player-ghi"]
with requests_mock_lib.Mocker() as m:
m.post(NOTIFICATION_URL, json={"id": "notif-id-001", "recipients": 3})
result = client.create_notification({
"include_player_ids": player_ids,
"headings": {"en": "Flash Deal"},
"contents": {"en": "40% off everything — 2 hours only"},
})
# Verify what was actually sent to OneSignal
sent_body = m.last_request.json()
assert sent_body["app_id"] == APP_ID
assert sent_body["include_player_ids"] == player_ids
assert sent_body["headings"]["en"] == "Flash Deal"
assert result["id"] == "notif-id-001"
assert result["recipients"] == 3
def test_send_stores_notification_id(service):
"""The returned notification ID must be persisted for delivery tracking."""
with requests_mock_lib.Mocker() as m:
m.post(NOTIFICATION_URL, json={"id": "notif-xyz-789", "recipients": 1})
notification_id = service.send_to_user(
user_id="user-42",
title="Your package arrived",
body="Pick up at locker B3"
)
assert notification_id == "notif-xyz-789"
# Service should have stored this in DB
assert service.notification_store.get("notif-xyz-789") is not NoneTesting Segment-Based Targeting
# test_onesignal_segments.py
import requests_mock as requests_mock_lib
import pytest
from myapp.onesignal_client import OneSignalClient
NOTIFICATION_URL = "https://onesignal.com/api/v1/notifications"
@pytest.fixture
def client():
return OneSignalClient(app_id="app-id", api_key="api-key")
def test_send_to_segment_uses_included_segments(client):
with requests_mock_lib.Mocker() as m:
m.post(NOTIFICATION_URL, json={"id": "notif-segment-001", "recipients": 5420})
client.create_notification({
"included_segments": ["Active Users", "Premium"],
"headings": {"en": "New Feature"},
"contents": {"en": "Check out our new dashboard"},
})
body = m.last_request.json()
assert "included_segments" in body
assert "Active Users" in body["included_segments"]
assert "Premium" in body["included_segments"]
# Should NOT include player_ids or external_user_ids when using segments
assert "include_player_ids" not in body
assert "include_external_user_ids" not in body
def test_send_to_all_users_uses_all_segment(client):
with requests_mock_lib.Mocker() as m:
m.post(NOTIFICATION_URL, json={"id": "broadcast-001", "recipients": 120000})
from myapp.push_service import OneSignalPushService
service = OneSignalPushService(app_id="app-id", api_key="api-key")
result = service.broadcast(title="App Update", body="Version 3.0 is live")
body = m.last_request.json()
assert body["included_segments"] == ["All"]
assert result["recipients"] == 120000
def test_excluded_segments_filter_correctly(client):
with requests_mock_lib.Mocker() as m:
m.post(NOTIFICATION_URL, json={"id": "targeted-001", "recipients": 3200})
client.create_notification({
"included_segments": ["All"],
"excluded_segments": ["Unsubscribed", "Test Devices"],
"headings": {"en": "Important Update"},
"contents": {"en": "Terms of service have changed"},
})
body = m.last_request.json()
assert "Unsubscribed" in body["excluded_segments"]
assert "Test Devices" in body["excluded_segments"]Testing A/B Test Notification Setup
OneSignal supports A/B testing via the ab_test_id field and variants in the payload:
# test_onesignal_ab_tests.py
import requests_mock as requests_mock_lib
import pytest
from myapp.push_service import OneSignalPushService
NOTIFICATION_URL = "https://onesignal.com/api/v1/notifications"
@pytest.fixture
def service():
return OneSignalPushService(app_id="app-123", api_key="key-456")
def test_ab_test_creates_notification_with_variants(service):
variants = [
{"contents": {"en": "Your cart is waiting"}, "weight": 50},
{"contents": {"en": "You left something behind"}, "weight": 50},
]
with requests_mock_lib.Mocker() as m:
m.post(NOTIFICATION_URL, json={
"id": "ab-test-notif-001",
"recipients": 800,
"ab_test_id": "ab-001"
})
result = service.send_ab_test(
segment="Cart Abandoners",
headings={"en": "Don't forget!"},
variants=variants
)
body = m.last_request.json()
assert "ab_test_id" in body or "variants" in body
assert result["id"] == "ab-test-notif-001"
def test_ab_test_weights_must_sum_to_100(service):
bad_variants = [
{"contents": {"en": "Version A"}, "weight": 60},
{"contents": {"en": "Version B"}, "weight": 60}, # sums to 120
]
with pytest.raises(ValueError, match="weights must sum to 100"):
service.send_ab_test(
segment="Active Users",
headings={"en": "Test"},
variants=bad_variants
)
def test_ab_test_requires_at_least_two_variants(service):
with pytest.raises(ValueError, match="at least 2 variants"):
service.send_ab_test(
segment="Active Users",
headings={"en": "Test"},
variants=[{"contents": {"en": "Only version"}, "weight": 100}]
)Testing Delivery Reporting
# test_onesignal_delivery.py
import requests_mock as requests_mock_lib
import pytest
from myapp.onesignal_client import OneSignalClient
from myapp.analytics_service import NotificationAnalytics
BASE_URL = "https://onesignal.com/api/v1"
@pytest.fixture
def client():
return OneSignalClient(app_id="app-id", api_key="api-key")
def test_get_notification_returns_delivery_stats(client):
notif_id = "notif-abc-123"
with requests_mock_lib.Mocker() as m:
m.get(
f"{BASE_URL}/notifications/{notif_id}",
json={
"id": notif_id,
"successful": 4821,
"failed": 12,
"errored": 3,
"converted": 289,
"remaining": 0,
"queued_at": 1716019200,
"send_after": 1716019200,
}
)
stats = client.get_notification(notif_id)
assert stats["successful"] == 4821
assert stats["failed"] == 12
assert stats["converted"] == 289
def test_analytics_calculates_delivery_rate(client):
notif_id = "notif-xyz-789"
analytics = NotificationAnalytics(client)
with requests_mock_lib.Mocker() as m:
m.get(f"{BASE_URL}/notifications/{notif_id}", json={
"id": notif_id,
"successful": 9000,
"failed": 1000,
"errored": 0,
"converted": 450,
"remaining": 0,
})
report = analytics.get_report(notif_id)
assert report["delivery_rate"] == pytest.approx(90.0)
assert report["conversion_rate"] == pytest.approx(5.0)
assert report["total_sent"] == 10000
def test_delivery_polling_retries_while_remaining_gt_zero(client, mocker):
"""Analytics poller should retry until remaining == 0."""
notif_id = "notif-polling-test"
analytics = NotificationAnalytics(client)
mock_sleep = mocker.patch("time.sleep")
with requests_mock_lib.Mocker() as m:
# First call: still sending
m.get(f"{BASE_URL}/notifications/{notif_id}", [
{"json": {"id": notif_id, "successful": 100, "failed": 0, "remaining": 500}},
{"json": {"id": notif_id, "successful": 550, "failed": 5, "remaining": 50}},
{"json": {"id": notif_id, "successful": 590, "failed": 10, "remaining": 0}},
])
report = analytics.poll_until_complete(notif_id, poll_interval=5)
assert report["successful"] == 590
assert mock_sleep.call_count == 2 # Slept twice before remaining hit 0Testing External User ID Targeting
# test_onesignal_external_ids.py
import requests_mock as requests_mock_lib
from myapp.push_service import OneSignalPushService
NOTIFICATION_URL = "https://onesignal.com/api/v1/notifications"
def test_send_to_external_user_id():
service = OneSignalPushService(app_id="app-id", api_key="api-key")
with requests_mock_lib.Mocker() as m:
m.post(NOTIFICATION_URL, json={"id": "notif-ext-001", "recipients": 1})
service.send_to_external_user(
external_user_id="user-db-id-12345",
title="Account Alert",
body="New login from Chicago, IL"
)
body = m.last_request.json()
assert body["include_external_user_ids"] == ["user-db-id-12345"]
# Must include channel_for_external_user_ids for proper routing
assert "channel_for_external_user_ids" in body
assert body["channel_for_external_user_ids"] == "push"Using httpretty for Global HTTP Interception
For tests where requests-mock context managers are too verbose, httpretty provides global interception:
# test_onesignal_httpretty.py
import httpretty
import json
import pytest
from myapp.onesignal_client import OneSignalClient
@httpretty.activate
def test_cancel_notification_sends_delete():
httpretty.register_uri(
httpretty.DELETE,
"https://onesignal.com/api/v1/notifications/notif-to-cancel",
body=json.dumps({"success": True}),
content_type="application/json"
)
client = OneSignalClient(app_id="app-id", api_key="api-key")
result = client.cancel_notification("notif-to-cancel")
assert result["success"] is True
assert httpretty.last_request().method == "DELETE"With this test suite, you can run pytest tests/onesignal/ -v on every commit and know that your OneSignal integration handles player targeting, segment broadcasting, A/B test configuration, and delivery analytics correctly — before those notifications ever reach a real user's device.