Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Testing

Assert

Assert.Equal(expected, actual[, precision]) · Assert.True(x) · Assert.NotNull(x) · Assert.Greater/Less/InRange · Assert.Matches · Assert.NotEmpty/Count · Assert.That · Assert.Throws(() => …) · Assert.Denied(() => …)

The assertions a test makes. Beyond the usual equality and null checks there are comparisons (Greater, Less, InRange), a regex check (Matches), collection checks (NotEmpty, Count), a general predicate (That) — and the two that earn their keep: Assert.Throws proves a rule is enforced and Assert.Denied proves security is.

stable5 examples compiled by CItesting

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 doubleAssert.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) and Assert.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) — the text matches the regular expression pattern — pattern first (Regex).
  • Assert.NotEmpty(collection) — the collection has at least one element; its opposite is Assert.Empty, and Assert.Single claims exactly one.
  • Assert.Count(collection, n) — the collection has exactly n elements. 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 boolean condition (first), with a value carried 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.

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

Related

Layout assertions — is it actually usable on screen?

Every other assertion is about TEXT or STATE, and all of them pass on a screen that is visually broken: a control can…

Ui — drive the app's UI from a test

Drive the real UI from a test: navigate to a route, click what a person would click, and assert on what the screen…

[Test] / [TestFixture]

A test is an ordinary function marked [Test]. It runs against a throwaway clone of the app, so it may create rows and…

runas

Runs a block as a given principal, so security rules apply exactly as they would for that user. It is how you test that…

invariant

A row-level rule spanning several members, checked when the row is written. Use it when a constraint on one member is…

secure by default (deny-all)

Deny-all is the posture, and it is the only one: an entity that declares no `security { }` block is denied to every…