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

[Test] / [TestFixture]

[TestFixture] void Seed() { … } [Test(Seed)] void It_does_the_thing() { Assert.Equal(…); }

A test is an ordinary function marked [Test]. It runs against a throwaway clone of the app, so it may create rows and break rules freely. A [TestFixture] seeds the data once and every test forks from it.

stable6 examples compiled by CItesting

Summary#

A test is an ordinary function marked [Test]. It runs against a throwaway clone of the app — its own database, made for it and thrown away after — so it can create rows, violate rules and assert on the wreckage without touching anything real, and without cleaning up after itself.

A [TestFixture] builds the starting data once; every test that names it forks from that state.

Signature#

[TestFixture]
void <Seed>() { … }               // build the starting data, once

[Test]
void <Name>() { … }               // a test with no fixture

[Test(<Seed>)]
void <Name>() { … }               // a test that starts from <Seed>'s data

Description#

A test is a function#

There is no separate test language. A test is a function: it can call your functions, create rows, run queries — the whole model is in scope.

entity Order {
  [Required] string Code;
  decimal Total;

  security { allow create, read when IsAuthenticated || IsAnonymous; }   // an entity with no security block is denied to everyone
}

void PlaceOrder(string code, decimal total) {
  var o = 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);
}

A row a function hands back is a live row of the test's unit of work

A function runs in a child scope of its caller's unit of work, committed upward when it returns. The row it returns — created there, or merely loaded there — is the same live row the test would have got from a query: assign to it and the write is staged in the test's unit of work, and the next Assert.* flushes it like any other. Nothing to re-read, nothing to re-attach.

entity Organization {
  [Required, MaxLength(100)] string Name;
  [Required, Unique, MaxLength(60)] string Slug;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

Organization Found(string name, string slug) {
  var org = new Organization { Name = name, Slug = slug };
  UnitOfWork.Commit();      // with or without this — a merely staged return value is tracked the same way
  return org;
}
[Test]
void A_returned_row_is_still_the_tests_row() {
  var acme = Found("Acme", "acme");
  acme.Name = "Acme Travel";                                          // a staged write in THIS test's unit of work
  Assert.Equal("Acme Travel", Organization.Single(o => o.Slug == "acme").Name);   // read back through a fresh query
}

The one row that is not live is one created in a scope that then faulted — a try body or a call that threw after making it. That row never existed, and reading or writing it is refused by name: the Organization row … was created in a scope that has since been discarded. Create it where it is wanted, or catch the fault inside the scope that created it.

The same is true after an Assert.Throws or Assert.Denied catches a commit fault. The refusal that assert was written to prove is a failed transaction, so everything the test had staged and not committed is thrown away with it — otherwise the same violation trips again at the next assert and errors the test after it has already passed. A row the test created before that point is therefore gone too, and touching it afterwards says so: the Organization row … was created and then DISCARDED. Seed through a [[#fixture|[TestFixture]]], or commit what you want to keep, before the assert that expects a fault.

A fixture seeds once; tests fork from it#

Building the same three customers at the top of nine tests is slow and, worse, it is nine places to change. A [TestFixture] builds them once:

[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());     // this test's own clone: the seeded two, plus this one
}

The second test creates a third order and sees three. The first test still sees two. Tests never see each other's writes — each forks its own copy of the fixture's data, so they can run in any order, or at the same time, and neither has to undo anything.

That isolation is the whole point: a test suite where one test's leftovers change another's outcome is a suite that fails at random and gets ignored.

The fixture is unsecured; the test body is not#

They do not run under the same rules, and this catches everyone once:

  • a [TestFixture] seeds unsecured — it can create rows across every entity, including ones nobody is allowed to create, so building a scenario never means weakening a rule;
  • a [Test] body runs secured, as an initially-anonymous principal — your rules are on, and nobody is signed in.

So a test that simply calls a function may be denied, and that is the system working. To act as a real user, name a principal and run as them ([runas(Name)] test attribute and principal selectors). The whole model is in the testing guide.

What should a test be called?#

A test's name is read by a person deciding whether the failure matters. A_new_order_is_not_seen_by_its_siblings tells them; Test3 does not.

Parking a test with [Skip]#

A test that cannot run yet — it depends on a capability the platform doesn't offer, or an intended behaviour that isn't built — is not deleted. Marking the [Test] with [Skip("reason")] keeps it in the suite as the executable record of the intended behaviour: it is still discovered and listed, but never run, and every surface renders it as a skip. The reason string is required — it is the visible note of why the test is parked:

[Test]
[Skip("KNOWN-GAP: refunds aren't implemented yet")]
void A_refund_restores_stock() {
  // The intended behaviour, written out — it compiles, so it can't rot, and it turns on
  // the day the gap closes and the [Skip] comes off.
  Assert.Equal(0, Order.Count());
}

[Skip] applies only to a [Test] (a [TestFixture] cannot be skipped), and its body must still compile — the whole value of a parked test is that it is a real, type-checked spec, not a comment.

See also#

Related

Assert

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

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…

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…

function

A function is a top-level unit of work, written like a C# method — a return type, a name, typed parameters, a body. It…