# Orchardcore Unit Test

> 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.

- Skill: `orchardcms/orchardcore-unit-test` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add orchardcms/orchardcore-unit-test`
- Raw SKILL.md: https://api.skillmd.com/api/skills/orchardcms/orchardcore-unit-test/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: orchardcms (https://skillmd.com/u/orchardcms)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/orchardcms/orchardcore-unit-test

---


# OrchardCore Unit & Integration Testing

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) |

## Decide the test type

| 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 |

## Workflow A: unit test

### Step 1: Add a test class

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

```csharp
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

```csharp
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:

```csharp
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.

```csharp
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:

```csharp
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`:

```csharp
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.

```csharp
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

```bash
# 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

| Attribute | Use |
|-----------|-----|
| `[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

| 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` |

### Moq cheatsheet

| 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` |

## 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

