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

Reference / Testing

Testing (real app, real data, real rules)

A test in Osy# is not a unit test with the world mocked out. It is your app, running against its own throwaway database, with your security rules switched on. That is what makes it worth writing — and it is why the one thing you must understand is who a test is ACTING AS: a test body runs secured, as an anonymous stranger, until you say otherwise.

stable8 examples compiled by CItestingguide

Summary#

A test is an ordinary function marked [Test]. It calls your real functions, against your real entities, with your real security rules enforced — on its own private copy of the database, thrown away when it finishes.

There is nothing to mock, because there is nothing in the way:

entity Order {
  [Required, Unique, MaxLength(20)] string Code;
  decimal Total;
  invariant Total >= 0;

  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

void PlaceOrder(string code, decimal total) {
  var order = new Order { Code = code, Total = total };
}
[Test]
void Placing_an_order_stores_its_total() {
  PlaceOrder("A1", 42m);

  Assert.Equal(42m, Order.Single(o => o.Code == "A1").Total);
}

No repository to stub, no in-memory database to configure, no test double for your own model. You wrote the function; the test calls it.

Description#

What a test actually runs against#

Before anything executes, the platform copies your app's database, compiles your local source and your tests into that copy, and throws it away at the end. Nothing a test writes can outlive it, and nothing you have deployed can mislead you — what runs is what is on your disk.

Inside that copy, the structure mirrors how you wrote the tests:

  • a [TestFixture] builds the starting data once;
  • every [Test] that names it forks its own private clone of that seeded state.

So two tests under the same fixture never see each other's writes, in any order, at the same time. That isolation is the whole point: a suite where one test's leftovers change another's outcome is a suite that fails at random and then gets ignored.

[TestFixture]
void Seeded() {
  PlaceOrder("A1", 100m);
  PlaceOrder("A2", 50m);
}

[Test(Seeded)]
void The_seeded_orders_are_there() {
  Assert.Equal(2, Order.Count());
}

[Test(Seeded)]
void A_new_order_is_not_seen_by_its_siblings() {
  PlaceOrder("A3", 5m);

  Assert.Equal(3, Order.Count());   // its OWN clone: the seeded two, plus this one
}

The second test creates a third order and sees three. The first still sees two. Neither undoes anything.

The one thing to understand: who is the test acting as?#

This is the paragraph that will save you an afternoon. The fixture and the test body do not run under the same rules, and the difference is deliberate:

SecurityWhy
[TestFixture]UNSECUREDit is scaffolding. It seeds freely across every entity, including ones nobody is allowed to create, so that setting up a scenario never requires weakening a rule.
[Test] bodySECURED, as an initially-anonymous principalit is the thing under test. Your rules are switched on, and nobody is signed in — so a denial is a real denial.

Two consequences, and they explain almost every surprise a newcomer hits:

1. A test that just calls a function may be denied — and that is the system working. An app is deny-all by default, and a [Test] is a stranger:

[Principal]
entity User {
  [Required, MaxLength(60)] string Name;
  security { allow read where Id == user.Id; }
}

entity Ledger {
  [Required, MaxLength(80)] string Entry;
  security { allow create, read when IsAuthenticated; }
}

void Record(string entry) {
  var line = new Ledger { Entry = entry };   // no auth code — the rule does the work
}
[Test]
void An_anonymous_caller_cannot_write_to_the_ledger() {
  Assert.Denied(() => Record("payroll"));

  Assert.Empty(Ledger.ToList());
}

2. To test what a real user does, you must ACT AS one. That is what [[testing-runas-attribute|[runas]]] is for, and it is the only way security gets tested at all.

Testing the rules — the part nobody else can do for you#

A security rule is the one kind of code whose bugs are invisible until they are catastrophic. A rule that is too strict fails loudly the first time someone uses the app. A rule that is too loose fails silently, forever.

So: declare a principal — a name for a seeded [Principal] row — and run the test as them.

[Principal]
entity User {
  [Required, MaxLength(60)] string Name;
  security { allow read where Id == user.Id; }
}

entity Doc {
  [Required] User Owner;
  [MaxLength(200)] string Title;
  security { allow read where Owner == user; }   // you see your own documents. Nobody else's.
}
principal Alice => User.Single(u => u.Name == "Alice");
principal Bob   => User.Single(u => u.Name == "Bob");

[TestFixture]
void Seed() {
  // The fixture is UNSECURED, so it can seed rows that nobody is allowed to create.
  var alice = new User { Name = "Alice" };
  var bob   = new User { Name = "Bob" };
  var a = new Doc { Owner = alice, Title = "alice-doc" };
  var b = new Doc { Owner = bob,   Title = "bob-doc" };
}

[Test(Seed)]
[runas(Alice)]
void Alice_sees_her_own_document() {
  Assert.Single(Doc.ToList());
  Assert.Equal("alice-doc", Doc.Single().Title);
}

[Test(Seed)]
[runas(Bob)]
void Bob_cannot_see_Alices_document() {
  // Not "is hidden in the UI" — the row is not SELECTED. The rule is inside the query.
  Assert.Null(Doc.FirstOrDefault(d => d.Title == "alice-doc"));
}

[Test(Seed)]
void A_stranger_sees_nothing_at_all() {
  Assert.Empty(Doc.ToList());
}

Three tests, and between them they pin the rule from every side: the owner sees it, another user does not, and a stranger sees nothing. [runas] binds, never creates — the selector must resolve to a row the fixture seeded, so a denial test can never quietly pass against a principal production would never grant.

A rule you have not tested is a rule you only believe you wrote.

What to assert#

The everyday assertions are equality and null checks. The two that earn their keep are the ones that prove a refusal:

  • Assert.Denied(() => …) — the acting principal is refused by a security rule. This is how you prove a rule bites.
  • Assert.Throws<T>(() => …) — the code faults: your own throw, or a broken invariant or constraint arriving as a ValidationException.

The distinction matters: Denied means you were not allowed, Throws means it was not valid. A test that confuses them will pass for the wrong reason. See Assert for the full set.

Testing what the SCREEN does is Ui — drive the app's UI from a testUi.Visit opens a route, Ui.Click presses what a person would press, and the same Assert.* verbs ask the questions. It is the same test, so nothing on this page stops applying; within: is how you address one row when several read alike.

And what the screen does is not the same question as whether a person can USE it. Every assertion above is about text or state, and all of them pass on a page that is visually broken — a button under an overlay, a label cut off by its own box. Layout assertions — is it actually usable on screen? is the vocabulary for that, checked by osy test --pixels in a real browser; under plain osy test those claims report themselves NOT CHECKED rather than green.

[Test]
void An_order_cannot_have_a_negative_total() {
  Assert.Throws<ValidationException>(() => PlaceOrder("A9", -1m));

  Assert.Empty(Order.ToList());   // and the refused row was not left behind
}

Why does an assert commit my writes?#

Before every Assert.*, the platform commits whatever the test has written so far — which is why a test reads real, stored rows and never needs a UnitOfWork.Commit() of its own.

It has one consequence worth knowing, because it will otherwise confuse you for half an hour: an assert changes the state the next line runs against. If you are testing something that depends on work being uncommitted — how a query treats a pending edit, say (Querying data) — an assert placed before it will have settled that work, and you will observe the committed behaviour instead.

So put the act you are testing before the assertions about it, and give each scenario its own [Test] (they fork their own copies anyway, so this costs nothing).

Parking a test you cannot write yet#

[Skip("reason")] parks a [Test], and the reason is required. Use it when the behaviour you want is not expressible yet — the parked test is the executable statement of what you meant, and it surfaces as a real skip in the run rather than vanishing. Deleting it would delete the only record that the gap exists.

Running them#

osy test compiles your source and your tests together and runs them against a throwaway copy, streaming each result as it finishes. Tests run only against a Development app — see Running tests and Running tests locally, and Debugging tests locally when one is failing and you want to stop inside it.

See also#

Related

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

Assert

The assertions a test makes. Beyond the usual equality and null checks there are comparisons (Greater, Less, InRange)…

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…

Outbound calls in a test

A `[Test]` reaches the network for real — `Http.*` and a typed `client { }` operation both run inside a test, against…

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…

[runas(Name)] test attribute and principal selectors

A test runs deny-all as an anonymous principal, so to read or write real data it must act AS a seeded `[Principal]`…

Running tests

Runs your app's tests against a throwaway copy of its database, reporting each test as it finishes. Your local source…

Running tests locally

Runs your app's tests against a Platform on your own machine — no account, no network, no setup beyond a running local…

Debugging tests locally

Debugs one of your app's tests against a Platform on your own machine — breakpoints, stepping, and variable inspection…

The security model

How authorization works in Osy#, end to end. Everything is denied until you grant it; a grant is compiled into every…

Functions (the unit of work)

A function is where your app's logic lives — a top-level unit of work, written like a C# method, that runs on the…