OrchardCore Unit & Integration Testing

SkillWeb & browsing

Lets your agent write and run tests for OrchardCore apps, including unit, integration, and browser tests.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the OrchardCore Unit & Integration Testing skill

About this capability

Writes and runs OrchardCore tests — xUnit unit tests, SiteContext-based integration tests, Moq mocking, and Playwright functional tests. Use when the user needs to add a test, set up a test harness/tenant for tests, mock OrchardCore services, or run the test suite.

What this skill tells your AI

The instructions your AI receives, as published by orchardcms/orchardcore in .agents/skills/orchardcore-unit-test/SKILL.md and read by ahel’s review.

This skill guides you through writing and running OrchardCore tests following project conventions.

OrchardCore uses xUnit v3 (Microsoft Testing Platform — test projects are Exe). Tests live under test/:

ProjectKind
OrchardCore.Testsunit + in-process integration (main)
OrchardCore.Abstractions.Testspure unit tests for core abstractions
OrchardCore.Tests.Integrationexternal-service integration (S3, etc.)
OrchardCore.Tests.FunctionalPlaywright browser automation
OrchardCore.BenchmarksBenchmarkDotNet (not xUnit)

Decide the test type

Testing…TypeHarness
Pure logic, a single classunitplain xUnit + Moq
A driver/service needing DIunitbuild a small ServiceCollection
Content APIs, recipes, tenant behaviorintegrationSiteContext
Admin/front-end through a browserfunctionalOrchardTestFixture + Playwright

Workflow A: unit test

Step 1: Add a test class

Naming: {Subject}Tests; methods {Action}_{Condition}_{ExpectedResult}, for example Write_WithinLimit_Succeeds.

namespace OrchardCore.Json.Nodes.Test;

public class Base64Tests
{
    [Theory]
    [InlineData("YTw+OmE/", "a<>:a?")]
    [InlineData("SGVsbA==", "Hell")]
    public void DecodeToString_ValidBase64_ReturnsDecodedString(string source, string expected)
    {
        Assert.Equal(expected, Base64.FromUTF8Base64String(source));
    }
}

Use [Fact] for single cases, [Theory] + [InlineData]/[MemberData] for parameterized. Assertions are xUnit Assert.* (no Shouldly in this repo).

Step 2: Mock dependencies with Moq

using Moq;

// Quick stub:
var clock = Mock.Of<IClock>(c => c.UtcNow == DateTime.UtcNow);

// With setup/verify:
var shellHost = new Mock<IShellHost>();
shellHost.Setup(h => h.GetScopeAsync(It.IsAny<string>())).ReturnsAsync(scope);
// ...
shellHost.Verify(h => h.GetScopeAsync("Default"), Times.Once);

Build a service provider when the unit needs DI:

var httpContext = new DefaultHttpContext
{
    RequestServices = new ServiceCollection()
        .AddSingleton(myService.Object)
        .BuildServiceProvider(),
};

Workflow B: integration test with SiteContext

SiteContext spins up a real tenant (SQLite by default) from a recipe and gives you an HttpClient + tenant scope.

public class BlogPostApiControllerTests
{
    [Fact]
    public async Task CreateDraft_ExistingContentItem_CreatesDraft()
    {
        using var context = new SiteContext();
        await context.InitializeAsync();

        var response = await context.Client.PostAsJsonAsync("api/content?draft=true", contentItem);
        var draft = await response.Content.ReadAsAsync<ContentItem>();

        Assert.True(draft.Latest);
        Assert.False(draft.Published);
    }
}

Resolve tenant services inside a scope:

await context.UsingTenantScopeAsync(async scope =>
{
    var session = scope.ServiceProvider.GetRequiredService<ISession>();
    var posts = await session.Query<ContentItem, ContentItemIndex>(x => x.ContentType == "BlogPost").ListAsync();
    Assert.Equal(2, posts.Count());
});

Customize the recipe by subclassing or WithRecipe:

public class AgencyContext : SiteContext
{
    public AgencyContext() => this.WithRecipe("Agency");
}

Defaults: RecipeName = "Blog", DatabaseProvider = "Sqlite", a random tenant name + table prefix per test.

Workflow C: functional test (Playwright)

OrchardTestFixture starts a CMS server and a headless Chromium browser.

var page = await fixture.CreatePageAsync();
await page.GotoAsync("/");
await Expect(page.Locator("h1")).ToBeVisibleAsync();

Set PLAYWRIGHT_TRACING to capture screenshots/snapshots/sources into traces/.

Running tests

# All tests in a project (from repo root)
dotnet test test/OrchardCore.Tests/OrchardCore.Tests.csproj

# Filter by name (xUnit / MTP)
dotnet test test/OrchardCore.Tests/OrchardCore.Tests.csproj --filter "FullyQualifiedName~BlogPost"

CI requires all tests green. If you change CSS/JS, run yarn build first (asset tests).

Quick Reference

xUnit attributes

AttributeUse
[Fact]one test case
[Theory] + [InlineData]inline parameter sets
[Theory] + [MemberData(nameof(X))]computed parameter sets

Common assertions

Assert.Equal, Assert.True/False, Assert.Null/NotNull, Assert.Contains, Assert.Throws<T>, await Assert.ThrowsAsync<T>(...).

SiteContext members

MemberPurpose
InitializeAsync()create + set up the tenant
ClientHttpClient bound to the tenant
UsingTenantScopeAsync(fn)run code in the tenant's DI scope
GraphQLClientGraphQL API client
RecipeName / DatabaseProvideroverride before InitializeAsync

Moq cheatsheet

NeedCode
Stub a propertyMock.Of<I>(x => x.P == v)
Setup a methodm.Setup(x => x.F(It.IsAny<T>())).ReturnsAsync(r)
Verify a callm.Verify(x => x.F(arg), Times.Once)
Pass the objectm.Object

Gotchas

  • Test projects are Exe (MTP) — keep that OutputType when adding one; don't switch to library.
  • SiteContext is IDisposable — always using var context = ....
  • Resolve tenant services only inside UsingTenantScopeAsync; the outer scope isn't the tenant.
  • Integration tests use SQLite + a fresh per-test table prefix; tests must not assume shared state.
  • Guard refactors with tests — the contributing guide requires new tests for refactoring.

References

  • references/testing.md — SiteContext internals, fixtures, Playwright, project layout
  • src/docs/contributing/contributing-code.md (repo) — test expectations
  • test/OrchardCore.Tests/ (repo) — real examples
  • AGENTS.md (repo root) — build commands

Signals

GitHub stars
8k
Forks
3k
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
orchardcore-unit-test
Source
github.com/orchardcms/orchardcore