Summary#
Assert.* is what a test claims. The everyday ones are equality and null checks; the two that earn their keep are
Assert.Throws (a rule really is enforced) and Assert.Denied (a principal really cannot do it).
Signature#
Assert.Equal(<expected>, <actual>) Assert.NotEqual(<a>, <b>)
Assert.Equal(<expected>, <actual>, <precision>) // equal to N decimal places
Assert.True(<bool>) Assert.False(<bool>)
Assert.Null(<value>) Assert.NotNull(<value>)
Assert.Contains(<needle>, <haystack>) Assert.StartsWith(<prefix>, <text>)
Assert.Greater(<a>, <b>) Assert.Less(<a>, <b>) // a > b · a < b
Assert.InRange(<value>, <low>, <high>) // low <= value <= high, inclusive
Assert.Matches(<pattern>, <text>) // the text matches the regex pattern
Assert.NotEmpty(<collection>) Assert.Count(<collection>, <n>) // at least one · exactly n
Assert.That(<bool condition>, <value>) // condition holds; value shown on failure
Assert.Throws(() => <expression>) // the code faults — with ANY fault
Assert.Throws<T>(() => <expression>) // …and it is a `T`; RETURNS the fault, so `.Message` / `.Type` read
Assert.Denied(() => <expression>) // the acting principal is refused
Assert.Throws(() => { <statements> }) // a SEQUENCE that should fault (block body)
Assert.Denied(() => { <statements> }) // a sequence the acting principal is refused
Assert.Refuses(<assertion>) // that ASSERTION fails — the one you point at a rule you want enforced
Assert.Refuses(<assertion>, <message>) // …and the refusal says this (CONTAINS, not word for word)Description#
The four you will reach for every time#
entity Order {
[Required] string Code;
decimal Total;
bool Cancelled;
}
void PlaceOrder(string code, decimal total) {
var o = new Order { Code = code, Total = total };
}
[Test]
void An_order_starts_uncancelled() {
PlaceOrder("A1", 42m);
var o = Order.Single(x => x.Code == "A1");
Assert.Equal(42m, o.Total); // expected first, actual second — as in xUnit
Assert.False(o.Cancelled);
Assert.NotNull(o.Code);
}Assert.Equal(expected, actual) — expected first. Get it backwards and the test still passes; it is the failure
message that lies to you, which you will discover at the worst moment.
Comparing a double — Assert.Equal(expected, actual, precision)#
The third argument is the number of decimal places both sides are rounded to before comparing — xUnit's own
overload. Reach for it whenever the value is a double:
Assert.Equal(0.0, Math.Sin(Math.PI), 12);
Assert.Equal(2.0, Math.Log(Math.Exp(2)), 12);Math.Sin(Math.PI) is not exactly 0, and Math.Log(Math.Exp(2)) is not exactly 2 — in any language. π and e are
not representable as doubles, so every transcendental inherits the error of its input. The tolerance is not
sloppiness; it is the only correct way to assert one.
A decimal pair rounds as decimal, never through a double — money is exactly where a detour through binary
floating point would reintroduce the imprecision you are trying to tolerate. And the two-argument form stays
exact: the precision widens the comparison only where you ask for it.
Assert.Throws — prove the rule bites#
A rule you never tested is a rule you hope you wrote. Assert that breaking it actually fails:
entity Account {
[Required] string Holder;
decimal Balance;
invariant Balance >= 0;
}
[Test]
void A_negative_balance_is_refused() {
Assert.Throws(() => new Account { Holder = "Ada", Balance = -1m });
}Without this test, deleting the invariant line breaks nothing that anyone notices — until a balance goes negative in
production.
Assert.Throws<T> — and prove it is the RIGHT fault#
Assert.Throws(…) holds for any fault, which is often one claim too weak: a test that means "this is refused
because the row already exists" also passes when the code under test throws because a field is null. Name the type
and the assertion says which:
entity Invitation {
[Required, Unique, MaxLength(40)] string Code;
}
[Test]
void A_duplicate_code_is_refused() {
var first = new Invitation { Code = "welcome" };
var ex = Assert.Throws<ValidationException>(() => new Invitation { Code = "welcome" });
Assert.Contains("Code", ex.Message);
}Two things it gives you that the untyped form does not. It fails on the wrong fault — if the code throws a
NotFoundException, the test says so by name instead of passing. And it returns the fault, so you can go on to
assert on ex.Message (what a human will read) and ex.Type (its name as a string).
The type argument is the same closed set throw uses (throw), and only these nine names:
| Type | What raises it |
|---|---|
Exception | the base — matches every one below, exactly like the untyped form |
NotFoundException | you asked for something that does not exist (app-raised) |
ValidationException | the entity's own declared rules refused the write — a broken [Unique] (a duplicate row, single-column or composite), [Required], [Pattern], [MaxLength], [Min], [Max], or an invariant. The platform raises this for you at the commit, so it is the one most tests assert |
ConflictException | the CURRENT STATE of the data refuses it — two writers colliding. Not a broken constraint, however much English calls a double booking a conflict |
OAuthConnectionFailedException | an external authorization handshake failed |
NotAuthorized | engine-raised — a workflow event's [Authorize], or a slot's candidate gate, said no |
RequirementsNotMet | engine-raised — a slot deposit's Requires criteria are not met yet |
WorkflowError | engine-raised — an awaited child workflow reached a terminal error |
WorkflowCancelled | engine-raised — an awaited child workflow reached a terminal cancel |
Anything else is a compile error that lists the set with these same meanings, so you cannot misspell your way into a silent catch-all.
⚠ A security refusal is [[testing-assert#denied|Assert.Denied]], not a type argument here — there is no
SecurityException in the set, deliberately.
The block-body form works the same way: Assert.Throws<ValidationException>(() => { … }).
Assert.Denied — prove security bites#
The security equivalent, and the most valuable assertion in the set. It claims that the acting principal is refused — not that the code faulted, but that it was not allowed:
[Principal] entity User {
[Required] string Name;
}
entity Memo {
User Owner;
[MaxLength(200)] string Note;
security {
allow read when IsAuthenticated; // colleagues can SEE it …
allow update where Owner == user; // … only the owner may change it
}
}
void Annotate(Guid memoId, string note) {
var m = Memo.Single(x => x.Id == memoId);
m.Note = note;
}
The test — and every line of the setup is placed so that it cannot be the thing that fails:
// A `principal` resolves UNSECURED, which is what makes it usable here: a `[Test]` body outside a `runas` is an
// anonymous caller, so a `User.Single(…)` written there reads nothing and there is nobody to become.
principal Alice => User.Single(u => u.Name == "Alice");
principal Bob => User.Single(u => u.Name == "Bob");
[TestFixture]
void Seed() {
var alice = new User { Name = "Alice" };
var bob = new User { Name = "Bob" };
var memo = new Memo { Owner = alice, Note = "original" };
}
[Test(Seed)]
[runas(Alice)]
void Bob_cannot_annotate_Alices_memo() {
var memo = Memo.Single(m => m.Owner == Alice);
runas(Bob) {
// Bob can READ this row — that is deliberate, and it is what makes the assertion mean something. The only
// thing he may not do is WRITE it, so the refusal `Assert.Denied` catches can only be the update rule.
Assert.NotNull(Memo.FirstOrDefault(m => m.Id == memo.Id));
Assert.Denied(() => Annotate(memo.Id, "hijacked"));
}
}This is the test that stops a refactor from quietly opening a door. Write one for every rule that matters — see runas.
Notice where the setup lives: Alice, Bob and the memo are created by the fixture, and the row is looked up as
Alice before the runas(Bob). Notice too that Memo is READABLE by any colleague — if a non-owner could not
even see the row, Annotate would fail looking it up and Assert.Denied would pass on a refusal that has nothing
to do with the update rule the test names. Only the one thing that must be refused is inside Assert.Denied. That is not style — it is what makes the
assertion mean anything.
When an UPDATE is refused. A write is judged when it reaches the store, not when the property is assigned — so
inside Assert.Denied use the block form and commit in it: Assert.Denied(() => { memo.Note = "x"; UnitOfWork.Commit(); }).
A bare assignment with no commit inside the lambda is not yet a write the rule can refuse, and the assertion would
report that nothing was denied.
A denial test is the one test whose green tells you nothing by itself. Every other assertion proves it ran by producing the right answer; this one is satisfied by any refusal on the way to the thing you are testing. Build a prerequisite inside the lambda and the platform may refuse that instead — the test passes, the rule you named was never evaluated, and nothing distinguishes the two.
So: everything in the setup must be independently known-allowed for the acting principal. Seed prerequisites in a
[TestFixture], or create them as a principal already permitted to. And pair a denial with a positive twin that does the same thing successfully — if both fail the same way, the setup is what you are testing.
// ✗ Creating the User is itself a write Alice may be refused for. If it is, the denial fires there
// and Membership's rule is never reached — green, and about nothing.
runas(alice) {
Assert.Denied(() => new Membership { Member = new User { Name = "Mallory" } });
}osy lint reports this as testing-denial-provable-by-its-setup: a denial whose lambda constructs more than one
entity. Constructing exactly the subject is the correct shape and is never flagged.
A block body — a sequence that should fault#
When the code you expect to fail is more than one expression — set something up, then do the thing that must be
refused — give Assert.Throws / Assert.Denied a block lambda instead of a single expression:
entity Ledger {
[Required] string Name;
decimal Balance;
invariant Balance >= 0;
}
[Test]
void An_overdraw_is_refused() {
var l = new Ledger { Name = "ops", Balance = 100m };
Assert.Throws(() => {
var current = l.Balance; // a local
l.Balance = current - 250m; // the write that trips `invariant Balance >= 0`
});
}The block runs its statements in order and the assertion holds if any of them faults. Keep it a simple sequence —
locals, assignments and calls (the work you expect to throw). Control flow (if, foreach) and return don't belong
in a deferred-assert block; if you need them, put them in a helper function and call it inside the lambda.
A block lambda is accepted only in these deferred-assert positions. Everywhere else — notably a query predicate
like Order.Where(o => …), whose body lowers to SQL — a lambda takes an expression body (o => o.Total > 0), and a
block there is a compile error that says so.
Comparisons, collections, and a general predicate#
Beyond equality there is a small kit for the everyday shapes of a claim:
Assert.Greater(a, b)andAssert.Less(a, b)— the first value is strictly greater / less than the second.Assert.InRange(value, low, high)— the value lies within[low, high], bounds included.Assert.Matches(pattern, text)— thetextmatches the regular expressionpattern— pattern first (Regex).Assert.NotEmpty(collection)— the collection has at least one element; its opposite isAssert.Empty, andAssert.Singleclaims exactly one.Assert.Count(collection, n)— the collection has exactlynelements. This is the honest way to assert "the query returned three rows", and under a security rule it counts only the rows the acting principal may see.Assert.That(condition, value)— the escape hatch: assert an arbitrary booleancondition(first), with avaluecarried along to appear in the failure message. Use it for a claim none of the named assertions captures.
[Test]
void Comparison_and_collection_assertions() {
Assert.Greater(5, 3); // 5 > 3
Assert.Less(3, 5); // 3 < 5
Assert.InRange(5, 1, 10); // within [1, 10]
Assert.InRange(1, 1, 10); // the bounds are inclusive
Assert.Matches("[a-z]+[0-9]+", "abc123"); // pattern first: "abc123" matches the regex
var parts = Text.Split("a,b,c", ","); // a List<string> of three
Assert.NotEmpty(parts);
Assert.Count(parts, 3); // exactly three elements
var total = 42m;
Assert.That(total > 0m && total < 100m, total); // condition first; `total` is shown if it fails
}Prefer a named assertion when one fits — Assert.Count(rows, 3) reads better and fails with a clearer message than
Assert.That(rows.Count == 3, rows.Count). Keep Assert.That for the claim that has no better name.
The assertions that read the SCREEN#
These nineteen live on the same Assert. and are absent from everything above — a test that drives the UI reaches
for them, and a reader who came here for "what can I assert?" would otherwise conclude they do not exist. Their
detail, and the Ui.* verbs that drive the screen they read, are in Ui — drive the app's UI from a test.
| assertion | says |
|---|---|
Assert.Visible(text) | the text is rendered on the current screen |
Assert.Hidden(text) | it is not |
Assert.TextIs(text) | some element reads EXACTLY this — not merely contains it |
Assert.OnPage(path) | the app is currently showing this route |
Assert.Value(field, expected) | the field this label names holds this value |
Assert.Enabled(control) · Assert.Disabled(control) | the control this label names is (not) operable |
Assert.DisabledBecause(control, reason) | it is not operable, and explains itself with this sentence |
Assert.Items(container, expected) | the list this label names shows exactly this many items |
Assert.Dialog(title) | a dialog is open, and this is its title |
Assert.Checked(field[, expected]) | the checkbox or toggle is in this state — checked unless you pass false |
Assert.Expanded(control[, expected]) | the disclosure control is open (or closed) — open unless you pass false |
Assert.Selected(option[, expected]) | the option says it is the chosen one (or is not) — chosen unless you pass false |
Assert.Focused(control) | this is the control the keyboard is on |
Assert.Before(first, second) | the first value's row is rendered ABOVE the second's — the assertion a SORT needs |
Assert.Cell(row, column, expected) | that row's cell under this column header reads this |
Assert.Probe(control, field, expected) | a foreign control reports this field of its probe { } block as this |
Assert.Flow(container, direction) | that container lays its children out "across" or "down" |
Assert.Violation(field[, message]) | this field is refused, and the message it shows contains this |
Any of them may be scoped to one region with within: — Assert.Value("Name", "Ada", within: "Edit book") —
except the three a region cannot narrow: OnPage (a route is not inside a container), Dialog (a modal is
page-level), and Violation. Ui.Within(container) { … } scopes a whole block at once.
See also#
- Layout assertions — is it actually usable on screen? — the geometric assertions (
Assert.Clickable,Assert.FitsOn,Assert.Above, …), which need a renderer with a compositor and report NOT CHECKED without one - Ui — drive the app's UI from a test — the
Ui.*verbs, and the detail of every screen-reading assertion above - Regex — the pattern language
Assert.Matchesuses - [Test] / [TestFixture] — the
[Test]functions these assertions live in - runas — acting as a principal, so
Assert.Deniedhas someone to deny - invariant — the rules
Assert.Throwsproves you wrote