How to: Run Tests¶
Overview¶
SyRF has automated tests for both .NET services (xUnit) and the Angular web service (Vitest). Tests run automatically in CI/CD on pull requests and before deployments.
Test Infrastructure¶
| Service Type | Framework | Test Count | Location |
|---|---|---|---|
| Angular (Web) | Vitest | 612+ | src/services/web/ |
| .NET (API, PM, etc.) | xUnit | 5 projects | src/services/*/, src/libs/*/ |
Running Tests Locally¶
Using the Test Script¶
A unified test runner script is available:
# Run all tests
.github/scripts/run-tests.sh
# Run only Angular tests
.github/scripts/run-tests.sh web
# Run only .NET tests
.github/scripts/run-tests.sh dotnet
# Run with coverage
.github/scripts/run-tests.sh all --coverage
Running Angular Tests Directly¶
cd src/services/web
# Run tests once
pnpm exec ng test --no-watch
# Run with coverage
pnpm exec ng test --no-watch --coverage
# Run in watch mode (development)
pnpm exec ng test
Coverage output: src/services/web/coverage/
Running Specific Angular Tests¶
Use --include to run specific test files and --filter to match test names:
# Run a specific test file
pnpm exec ng test --no-watch --include="src/app/core/services/signal-r/signal-r.service.spec.ts"
# Run all tests in a folder (glob pattern)
pnpm exec ng test --no-watch --include="src/app/core/services/**/*.spec.ts"
# Filter by test name (regex matches describe/it blocks)
pnpm exec ng test --no-watch --filter="_connect"
# Combine file and name filtering
pnpm exec ng test --no-watch \
--include="src/app/core/services/signal-r/signal-r.service.spec.ts" \
--filter="_connect"
# Watch mode with specific file
pnpm exec ng test --include="src/app/core/services/signal-r/signal-r.service.spec.ts"
Note: Always use pnpm exec ng test (not pnpm exec vitest directly) to ensure Angular's build system properly compiles decorators and handles test teardown.
Running .NET Tests Directly¶
# Run all .NET tests
dotnet test syrf.sln
# Run with coverage
dotnet test syrf.sln \
--collect:"XPlat Code Coverage" \
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura
# Run specific service tests
dotnet test src/services/api/SyRF.API.Endpoint.Tests/
Running Specific .NET Tests¶
# Run a specific test project
dotnet test src/services/api/SyRF.API.Endpoint.Tests/
# Filter by test name (supports wildcards)
dotnet test --filter "FullyQualifiedName~SearchImportJob"
# Filter by test class
dotnet test --filter "ClassName=SearchControllerTests"
# Filter by test method
dotnet test --filter "Name=ShouldReturnSearchResults"
# Combine project and filter
dotnet test src/services/project-management/SyRF.ProjectManagement.Endpoint.Tests/ \
--filter "FullyQualifiedName~SearchImportJobStateMachine"
Overriding the MongoDB Testcontainers image¶
Every MongoDB Testcontainers fixture resolves its image through one helper
(src/testing/MongoTestContainerImage.cs). It defaults to mongo:8.0, which is what CI runs.
Set SYRF_TEST_MONGO_IMAGE to use a different tag locally, for example on a host where
mongo:8.0 refuses to start:
SYRF_TEST_MONGO_IMAGE=mongo:8.2 dotnet test src/libs/project-management/SyRF.ProjectManagement.Mongo.Data.Tests/
Unset or blank values fall back to mongo:8.0. (E2E compose/scripts use the separate
E2E_MONGO_IMAGE variable.)
Starting a MongoDB Testcontainer¶
Start every Mongo container through MongoTestContainerStart.StartAsync(() => new MongoDbBuilder()...Build())
(src/testing/MongoTestContainerStart.cs), never with a bare StartAsync(). With credentials set,
the mongo image's entrypoint stops a temporary mongod and immediately starts the real one on the
same port. On a loaded runner the real mongod can lose that race, exit with code 48 ("Address
already in use") and surface as ContainerNotRunningException, or, in a replica-set fixture whose
start-time rs.initiate exec reaches the dead container, as a DockerApiException 409 "is not
running". The helper disposes the failed container and starts a fresh one, up to three attempts, for
those two shapes only; any other start failure is not retried.
Link the file into a test project the same way as MongoTestContainerImage.cs.
CI/CD Test Integration¶
Tests run automatically in two workflows:
1. Pull Request Tests (pr-tests.yml)¶
Runs on ALL pull requests to main:
PR opened/updated
↓
detect-changes
↓
test-dotnet (if .NET changed) ──────────────┬── test-summary
test-dotnet-mongo-data (if .NET changed) ───┤
test-web (if Angular changed) ──────────────┘
Key features:
- Runs on every PR (not just preview-labeled)
- Only tests changed services (saves time)
- Uploads test results and coverage as artifacts
- Fails PR if tests fail
2. Main Branch Tests (ci-cd.yml)¶
Runs before deployment on push to main:
detect-changes ─┬─ test-dotnet (if .NET changed) ──────────────┬── release-gate → build/tag → promote-to-staging
├─ test-dotnet-mongo-data (if .NET changed) ───┤
├─ test-web (if Angular changed) ──────────────┤
└─ validate-docs-indexes ──────────────────────┘
Key features:
- Tests must pass (or be unneeded) before anything is built, tagged or promoted
- The .NET unit suite is two parallel jobs, as in PR Tests (#3886):
test-dotnet("Test .NET Services", 35 minutes) runs every test project exceptSyRF.ProjectManagement.Mongo.Data.Tests, andtest-dotnet-mongo-data("Test .NET Services (Mongo.Data)", 50 minutes) runs that project alone. A failure or cancellation in either blocks the release - Artifacts:
dotnet-test-results-{sha},dotnet-test-results-mongo-data-{sha},dotnet-coverage-{sha},dotnet-coverage-mongo-data-{sha}(Cobertura XML) andweb-coverage-{sha} test-dotnet-integrationgates production promotion only; see CI/CD Workflow Overview
Coverage Thresholds¶
Angular tests have coverage thresholds configured in vitest.config.ts:
| Metric | Threshold |
|---|---|
| Statements | 50% |
| Branches | 40% |
| Functions | 50% |
| Lines | 50% |
Tests will fail if coverage drops below these thresholds.
Code Quality (SonarCloud)¶
Code quality and coverage analysis is performed by SonarCloud on every PR.
What You Get¶
- Quality Gate: Pass/fail status on PRs based on quality thresholds
- Coverage Analysis: Line and branch coverage visualization
- Code Smells: Detect maintainability issues
- Security Hotspots: Identify potential security vulnerabilities
- Duplications: Find duplicated code blocks
- PR Decoration: Inline comments on issues in changed code
Setup (One-Time)¶
- Create SonarCloud Account: Go to sonarcloud.io and sign in with GitHub
- Projects are already configured in SonarCloud:
syrf-web- Angular frontendsyrf-api- API servicesyrf-project-management- PM service- Add Secrets in GitHub repo settings → Secrets → Actions:
SONAR_TOKEN_WEB- Token for syrf-web projectSONAR_TOKEN_API- Token for syrf-api projectSONAR_TOKEN_PM- Token for syrf-project-management project
Coverage Sources¶
| Language | Format | Report Path |
|---|---|---|
| .NET | Cobertura XML | coverage/dotnet/**/coverage.cobertura.xml |
| Angular | LCOV | coverage/web/lcov.info |
Quality Gate Badges¶
Add to your service README:
Web (Angular):
[](https://sonarcloud.io/summary/new_code?id=syrf-web)
[](https://sonarcloud.io/summary/new_code?id=syrf-web)
API:
[](https://sonarcloud.io/summary/new_code?id=syrf-api)
[](https://sonarcloud.io/summary/new_code?id=syrf-api)
Project Management:
[](https://sonarcloud.io/summary/new_code?id=syrf-project-management)
[](https://sonarcloud.io/summary/new_code?id=syrf-project-management)
MongoDB Testcontainers fixtures: drop per-test databases¶
A collection fixture's Mongo container lives until every test class in that xUnit collection has
run, and so does every database a test creates on it, with one WiredTiger file per collection and
per index. A test that creates its own database on a shared fixture drops it when the test ends.
In SyRF.ProjectManagement.Mongo.Data.Tests, hold a ThrowawayDatabases, create databases with
Create(prefix) (or Name(prefix) for one opened through another client, such as
CreateIsolatedContext), and call DropAllAsync() from IAsyncLifetime.DisposeAsync(). A dropped
database's files close only at mongod's next checkpoint, so dropping bounds growth but does not
replace the container's raised open-files limit.
MongoDB Testcontainers fixtures: one client per fixture¶
MongoDbTestFixture and MongoDbReplicaSetTestFixture (SyRF.Testing.Common) hand out a
MongoContext through CreateContext(). MongoContext.Dispose() does not dispose its
MongoClient, so a context created per test leaks a connection pool (one mongod thread per
connection). About 84 such calls exhausted the CI container's 256-PID cap and killed the shared
MongoDB-RS replica set late in Test .NET (#3864).
CreateContext()with no argument returns one shared context per fixture; call it freely.CreateContext(resumeRepo)builds a dedicated context. The fixture tracks it and disposes its client when the fixture is disposed, but each one still holds a pool until then; do not call it in a loop.- Never
new MongoContext(...)in a test without disposingMongoClientyourself. - The CI container is capped at 2 GB, 2 CPUs and 256 PIDs (
DockerTestContainerPolicy) wheneverSYRF_TEST_JOB_KEYis set. Reproduce locally with that variable set; run the whole project at most once.
MongoDB Testcontainers fixtures: open files¶
WiredTiger keeps one file open per collection and per index, and a dropped collection's file stays
open until a later checkpoint reaps it. Docker's default soft nofile limit is 1024 and mongod does
not raise it. A shared fixture container that creates collections, indexes and per-test databases
quickly enough therefore fails with Too many open files (error_code 24): WiredTiger panics
(WT_PANIC), mongod aborts, and every later test in that xUnit collection fails, first with
EndOfStreamException and then with 30-second Connection refused server-selection timeouts.
This killed the shared MongoDB container in SyRF.ProjectManagement.Mongo.Data.Tests on most
runs on 2026-10-14. Memory was not the cause: the container peaked at about 230 MiB of its 2 GiB cap.
DockerTestContainerPolicysetsnofileto 65536 (soft and hard) on every container it configures, local and CI. It raises a limit rather than capping anything, so it does not depend onSYRF_TEST_JOB_KEY.- Every Testcontainers builder, including short-lived per-test containers, must call
.WithCreateParameterModifier(DockerTestContainerPolicy.ApplyStandard)(ApplySqlServerfor SQL Server). A container built without it keeps Docker's 1024 limit and has no owner label. - To see how close a run gets, count mongod's open files while it runs:
docker exec <container> sh -c 'ls /proc/1/fd | wc -l'.
Timeouts¶
| Test Suite | CI Timeout | Rationale |
|---|---|---|
| Angular | 5 minutes | Tests run in ~10s, pnpm install adds overhead |
| .NET | 10 minutes | Includes restore, build, and test |
.NET Test Projects¶
| Project | Tests |
|---|---|
SyRF.API.Endpoint.Tests |
API service tests |
SyRF.ProjectManagement.Endpoint.Tests |
PM service tests |
SyRF.AppServices.Tests |
AppServices library tests |
SyRF.ProjectManagement.Core.Tests |
PM Core library tests |
SyRF.WebHostConfig.Common.Tests |
WebHostConfig library tests |
Troubleshooting¶
Angular Tests Fail in CI but Pass Locally¶
Common causes:
- Missing PDF.js mocks: The vitest-setup.ts includes mocks for PDF.js
- Circular dependencies: Some components have placeholder tests due to circular imports
- Timing issues: Use
fakeAsyncandtickfor async operations
.NET Tests Fail¶
Common causes:
- Missing dependencies: Run
dotnet restore syrf.slnfirst - Build errors: Run
dotnet build syrf.slnbefore testing - Database dependencies: Tests should mock MongoDB connections
PDF-Agent Integration Tests Time Out During Teardown¶
Run the affected suite with individual test timings (Docker is required):
DOTNET_PROCESSOR_COUNT=4 SYRF_TEST_JOB_KEY=syrf-local-pdf-retry \
dotnet test src/services/pdf-agent/SyRF.PdfAgent.Tests/ -c Release \
--filter 'Category=Integration' --logger 'console;verbosity=normal'
Use a unique SYRF_TEST_JOB_KEY for each concurrent run. It labels that run's containers
and applies the same resource caps as CI.
The RabbitMQ/Quartz retry fixture starts hosted services through MassTransit's test
harness, not a Generic Host. Its Quartz registration must set
AwaitApplicationStarted = false: the fixture's application lifetime never raises
ApplicationStarted. Leaving the default enabled lets message assertions pass but
makes Quartz shutdown wait for the one-minute harness timeout for every test,
which would consume most of the main integration job's 45-minute budget (sized for
slower Pomegranate listeners, not for per-test shutdown waits). Fix that wait at the
fixture: never absorb it with a larger job timeout or shorten the test's assertion
timeout.
QuartzRetryFixtureLifecycleTests verifies scheduler startup and shutdown without
the host signal or Docker. The real-broker integration suite still verifies scheduled
redelivery; production Quartz configuration is unchanged.
Coverage Below Threshold¶
Fix: Add tests for uncovered code paths. Check src/services/web/coverage/lcov-report/index.html for coverage report.
Viewing Test Results¶
In GitHub Actions¶
- Go to Actions tab
- Select the workflow run
- Check test-dotnet or test-web job
- Download artifacts for detailed reports
Artifact Contents¶
| Artifact | Contents |
|---|---|
dotnet-test-results |
.trx test result files (test-dotnet) |
dotnet-test-results-mongo-data |
.trx test result files (test-dotnet-mongo-data) |
dotnet-coverage |
Cobertura XML coverage (test-dotnet) |
dotnet-coverage-mongo-data |
Cobertura XML coverage (test-dotnet-mongo-data) |
web-coverage |
lcov, cobertura, text coverage |
web-test-results |
JUnit XML test results |
Related Documentation¶
- CI/CD Workflow Overview - Complete CI/CD pipeline
- Web Service README - Angular development
- Test Workflows Locally - Using
actfor local testing