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.
72
89%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
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/:
| Project | Kind |
|---|---|
OrchardCore.Tests | unit + in-process integration (main) |
OrchardCore.Abstractions.Tests | pure unit tests for core abstractions |
OrchardCore.Tests.Integration | external-service integration (S3, etc.) |
OrchardCore.Tests.Functional | Playwright browser automation |
OrchardCore.Benchmarks | BenchmarkDotNet (not xUnit) |
| Testing… | Type | Harness |
|---|---|---|
| Pure logic, a single class | unit | plain xUnit + Moq |
| A driver/service needing DI | unit | build a small ServiceCollection |
| Content APIs, recipes, tenant behavior | integration | SiteContext |
| Admin/front-end through a browser | functional | OrchardTestFixture + Playwright |
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).
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(),
};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.
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/.
# 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).
| Attribute | Use |
|---|---|
[Fact] | one test case |
[Theory] + [InlineData] | inline parameter sets |
[Theory] + [MemberData(nameof(X))] | computed parameter sets |
Assert.Equal, Assert.True/False, Assert.Null/NotNull, Assert.Contains, Assert.Throws<T>, await Assert.ThrowsAsync<T>(...).
| Member | Purpose |
|---|---|
InitializeAsync() | create + set up the tenant |
Client | HttpClient bound to the tenant |
UsingTenantScopeAsync(fn) | run code in the tenant's DI scope |
GraphQLClient | GraphQL API client |
RecipeName / DatabaseProvider | override before InitializeAsync |
| Need | Code |
|---|---|
| Stub a property | Mock.Of<I>(x => x.P == v) |
| Setup a method | m.Setup(x => x.F(It.IsAny<T>())).ReturnsAsync(r) |
| Verify a call | m.Verify(x => x.F(arg), Times.Once) |
| Pass the object | m.Object |
Exe (MTP) — keep that OutputType when adding one; don't switch to library.SiteContext is IDisposable — always using var context = ....UsingTenantScopeAsync; the outer scope isn't the tenant.references/testing.md — SiteContext internals, fixtures, Playwright, project layoutsrc/docs/contributing/contributing-code.md (repo) — test expectationstest/OrchardCore.Tests/ (repo) — real examplesAGENTS.md (repo root) — build commandsb0e1e44
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.