17. Testing Guide
17.1 Test projects (Shumoul.Saas.NotificationFramework repo)
| Project | Stack | Purpose |
|---|---|---|
Shumoul.Notification.Tests.Unit | xUnit, NSubstitute, FluentAssertions | Pure logic — retry decisions, failure classification, provider resolution, dispatch isolation, delivery engine, campaign workflow execution |
Shumoul.Notification.Tests.Integration | xUnit, NSubstitute, FluentAssertions, in-memory fakes | Per-channel dispatch against fake repositories/persistence, verifying persistence side effects without a real database |
Shumoul.Notification.Tests.Host | xUnit | DI wiring — BuildServiceProvider(validateScopes: true), adapter resolution, singleton lifetimes |
Shumoul.Notification.Tests.Performance | BenchmarkDotNet | Microbenchmarks on hot-path retry decision logic |
Unit test classes (confirmed present in source at time of writing — 6 classes, up from an earlier 5-class baseline; exact per-class test-method counts should be confirmed by running the suite rather than assumed from an older report):
Domain\RetryDecisionPolicyTests.cs—ShouldRetry,GetNextDelaySeconds,IsDeadLetterEligibleDomain\NotificationFailureClassifierTests.cs— channel-aware failure-type classificationProviders\NotificationChannelProviderResolverTests.cs— registration, duplicate-channel detectionDispatch\NotificationDispatchServiceTests.cs— provider isolation, per-channel exception handlingDelivery\DeliveryEngineServiceTests.cs— idempotency, retry scheduling, dead-letter creationCampaign\CampaignWorkflowExecutorServiceTests.cs— campaign step execution (added after the original Phase 4 test baseline; confirms the campaign engine now has dedicated unit coverage)
Integration tests: DispatchIntegrationTests.cs, backed by Fakes/ in-memory implementations of
IRetryRepository, IDeadLetterRepository, InMemoryAttemptRepository, InMemoryPersistenceContext, and a
FakeRetryStrategy — exercises real dispatch-service code against fake persistence, verifying the right rows
would be written without needing SQL Server.
17.2 Running the suite
dotnet test E:\SaaS\Shumoul.Saas.NotificationFramework\Shumoul.Notification.Tests.Unit
dotnet test E:\SaaS\Shumoul.Saas.NotificationFramework\Shumoul.Notification.Tests.Integration
dotnet test E:\SaaS\Shumoul.Saas.NotificationFramework\Shumoul.Notification.Tests.Host
dotnet run --project E:\SaaS\Shumoul.Saas.NotificationFramework\Shumoul.Notification.Tests.Performance -c Release
17.3 Backend testing
Run the four commands above before releasing a new Shumoul.Notification.* package version — see
docs/ARCHITECTURE/TEST_STRATEGY.md and docs/ARCHITECTURE/REGRESSION_MATRIX.md for the platform's fuller
regression matrix (65+ rows as of the Phase 4 baseline) covering both this framework and its ERP-side
integration points.
17.4 Angular testing
No Angular-specific test harness ships with this framework (it is backend-only). When testing an Angular integration against it:
- Mock the
Result<T>/DtResult<T>envelope shapes documented in §9.0 in your HTTP interceptor tests, rather than hand-rolling ad-hoc response shapes. - For SignalR, use a fake
HubConnection(the@microsoft/signalrclient is mockable) to simulate a"messages"event firing, and assert your store/badge update logic reacts correctly — see §10.5.
17.5 Flutter testing
Similarly, mock the same Result<T> shapes for REST calls, and fake the HubConnection.on('messages', ...)
callback registration to unit-test your notification bloc/provider without a live server.
17.6 Postman / manual API testing
No Postman collection or .http file was found in the Shumoul.BackEnd\Shumoul repository at the time
of this documentation pass. Use the request/response examples throughout Chapter 9
directly as the basis for manual curl/Postman requests, or generate a collection from the platform's
Swagger/OpenAPI document (grouped by the GroupName values listed in §9.1).
17.7 SignalR testing
To manually verify real-time delivery end to end:
- Connect a test client (browser console with the Angular sample from §10.5, or a small standalone SignalR client) using a valid JWT for a known user.
- Trigger a dispatch for an event whose configuration has
EnableSignalR = true(§9.4) targeting that user'sRecipientUserId. - Confirm the
"messages"event fires client-side with the expectedUserNotificationDtopayload (§10.4). - Disconnect the client, trigger dispatch again, and confirm no client-side event fires (expected — SignalR
has no queued redelivery) but an
AppNotificationrow still appears viaGetInboxon reconnect if the InApp channel was also enabled for that event.
17.8 Failure simulation
To exercise the retry/dead-letter path described in Chapter 12 without waiting for a real provider outage:
- In a lower environment, temporarily misconfigure one channel's credentials (e.g. set an invalid
MailSettings.Password, or an invalidWhatsAppCloudApi.AccessToken). - Trigger a dispatch targeting that channel.
- Confirm a
NotificationDeliveryAttemptrow is created withSucceeded = falseand a populatedErrorCode/ErrorMessage. - Confirm a
NotificationRetryQueuerow appears withState = RetryScheduled, scheduled per that channel'sNotificationDeliveryPolicyretry strategy (see §9.5). - Wait for (or manually trigger via
RetryNow) the nextnotification-retry-processor-pipelinerun and confirm the attempt count increments. - Repeat until
MaxRetriesis exceeded, and confirm the row transitions toDeadLetterstate and a correspondingNotificationDeadLetterrow is created (ifDeadLetterEnabledis true for that policy). - Revert the credential misconfiguration before continuing any other testing.
17.9 Retry simulation
See §17.8 steps 3–5 — the same procedure exercises the retry engine specifically.
To test the atomic-claim/orphan-repair logic directly, this requires two concurrent worker instances racing
for the same NotificationRetryQueue row, which is impractical to simulate manually — rely on the unit tests
in Delivery\DeliveryEngineServiceTests.cs for that guarantee instead.
17.10 Dead letter simulation
See §17.8 step 6. To test Requeue
specifically, call it against the resulting dead letter and confirm a fresh NotificationRetryQueue row is
created from the preserved PayloadSnapshot.