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
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.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 where Owner == user; }
}
void Annotate(Guid memoId, string note) {
var m = Memo.Single(x => x.Id == memoId);
m.Note = note;
}
[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)]
void Bob_cannot_annotate_Alices_memo() {
var alice = User.Single(u => u.Name == "Alice");
var bob = User.Single(u => u.Name == "Bob");
var memo = Memo.Single(m => m.Owner == alice);
runas(bob) {
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 before
runas. Only the one thing that must be refused is inside Assert.Denied. That is not style — it is what makes the
assertion mean anything.
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