Osy#betaa language · its runtime Osyrin · a hosted platform
Why Osy#Built for agentsAgents as declarationsWorkflows that waitRuns exactly onceSecure by defaultNothing to mockThe editor is the compilerUI in the languageDocuments are dataOne 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.Throws<T>(() => …) · 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.

stable7 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 — 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 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.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:

TypeWhat raises it
Exceptionthe base — matches every one below, exactly like the untyped form
NotFoundExceptionyou asked for something that does not exist (app-raised)
ValidationExceptionthe 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
ConflictExceptionthe CURRENT STATE of the data refuses it — two writers colliding. Not a broken constraint, however much English calls a double booking a conflict
OAuthConnectionFailedExceptionan external authorization handshake failed
NotAuthorizedengine-raised — a workflow event's [Authorize], or a slot's candidate gate, said no
RequirementsNotMetengine-raised — a slot deposit's Requires criteria are not met yet
WorkflowErrorengine-raised — an awaited child workflow reached a terminal error
WorkflowCancelledengine-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) 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…