Osy#betaRun it on Osyrin: osyrin.com

Why Osy# · Chapter 06 · Testing

An agent can prove its code works. Not say so.

Code written by an agent has to be proven, not vouched for, and a proof is only as good as what it ran against. In Osy# a test runs the application: your functions over your entities, under the rules you declared, as a person the test names, from a starting state the test chose. What it exercised is the program, so what it proved is the program.

the reports example app · tests/reports.test.osyverbatim — this file compiles
[Test(TwoPeopleThreeReports)]
[runas(Anna)]
void anna_sees_her_two_reports() {
  Assert.Equal(2, MyReports().Count);
}
In practiceThe agent writes the test beside the feature and runs one command; the green it hands you ran the application as a person, under its rules, from a known starting state.

01

A test runs the application

The application from the last chapter: a User, an ExpenseReport that each employee may read only their own of, and MyReports(), which returns what its caller may see. A test of it is three declarations in one file.

Platform.Tools.SiteGen/content/apps/reports/tests/reports.test.osyverbatim — this file compiles
1[TestFixture]
void TwoPeopleThreeReports() {
  var anna = new User { Name = "Anna" };
  var otto = new User { Name = "Otto" };
  new ExpenseReport { Title = "Lisbon, March",  Employee = anna };
  new ExpenseReport { Title = "Berlin, April",  Employee = anna };
  new ExpenseReport { Title = "Oslo, May",      Employee = otto };
}

2principal Anna => User.Single(u => u.Name == "Anna");
principal Otto => User.Single(u => u.Name == "Otto");

[Test(TwoPeopleThreeReports)]
3[runas(Anna)]
void anna_sees_her_two_reports() {
4  Assert.Equal(2, MyReports().Count);
}
1

The fixture establishes the world. It creates rows through the application's own entities, with the rules off, because it is scaffolding: setting up a scenario never means loosening a rule.

2

A principal names somebody in that world. It is a query that finds a row the fixture made. It binds; it never creates.

3

The test runs the application as her. A test body on its own is an anonymous caller with the rules on; under runas, every read in it is Anna's.

4

An ordinary call to the application's own function, and the answer Anna gets: two rows, because the rule on ExpenseReport is where Employee == user.

The fixture establishes the world. The principal names somebody in it. The test runs the application as them. Everything on this page is that sentence with more of the application in scope.

02

A fixture is a starting state, and every test gets its own copy

A fixture seeds once. Every test that names it starts from a private copy of that state, so what one test writes, another never reads, in any order and at the same time. A test can create rows, break rules and leave the wreckage where it fell.

the whole suite of a real app, in one command
$ osy test
Starting a local platform for this project…

  ✓ a_note_is_invisible_to_everyone_but_its_owner (908ms)
  ✓ an_owner_still_sees_her_own_note (967ms)
  ✓ a_note_planted_for_somebody_else_is_not_even_EXPRESSIBLE (967ms)
  ✓ an_ITEM_is_invisible_when_its_list_is (627ms)
  ✓ an_item_cannot_be_added_to_somebody_elses_list (1103ms)
  … 16 more

✓ 21 passed.
⚠ the app's test set CHANGED since the last run — 24 declared then, 21 now:
    gone: shot_recall_lists, shot_recall_note, shot_recall_notes
  A count that holds still over a set that moved reads as "still green" while
  coverage moves.

Real output. Each of the 21 ran against its own copy of one fixture's data. The warning at the end is the runner noticing that the set of tests moved while the count did not.

The VS Code Test Explorer running an Osy# suite: six green tests grouped by file and fixture
The same tests from the editor. The tree is file → fixture → test: the fixture is a group because it is the state the tests under it start from.

03

A rule is proven by running as the people it governs

Chapter 05 put a rule inside every query. The rule on ExpenseReport is the one the test in §1 ran, and a rule has two sides: the person it admits and the people it excludes. Recall is a personal app, and its rule is that two people using one deployment never see a trace of each other.

Apps/recall-osy/tests/isolation.test.osyverbatim — this file compiles
[Test(TwoPeople)]
1[runas(Bo)]
void a_note_is_invisible_to_everyone_but_its_owner() {
2  Assert.Null(Note.FirstOrDefault(n => n.Title == "Ana's private note"));
3  // …and not by title alone: a listing must not leak it either.
  Assert.Equal(1, Note.Where(n => n.Title != "").Take(50).ToList().Count);
}
1

Run as the other person. The rule is inside the query, so Ana's note is not hidden from Bo after the fact: it is not selected.

2

By name: the row is not there for him.

3

…and by listing, which is the leak a lookup by name would miss.

4

The positive half. A rule that denied everyone passes every refusal test ever written and leaves the app unusable. The owner's test is what proves the rule is a rule and not a wall.

04

The same person reaches the screen

So far the test has established a state, named a person and run the application as them. Ui.SignInAs signs a browser in as that same person, and the test goes on from the page: Ui.Visit opens a route, Ui.Fill and Ui.Click do what she would do, and the same Assert.* verbs ask the questions.

demo/ember/tests/pages.test.osyverbatim — this file compiles
void a_message_you_send_appears_in_the_transcript_it_arrived_on() {
  Guid id = Guid.Empty;
1  runas (Ada) { id = CreateChannel("design", "how things look").Id; }

2  Ui.SignInAs(Ada);
  Ui.Visit("/c/" + id);

  Assert.Visible("Nothing here yet.");

3  Ui.Fill("Message", "morning — pushing the new rail today");
  Ui.Click("Send");

  Assert.Visible("morning — pushing the new rail today");
  Assert.Visible("ada");
4  Assert.Hidden("Nothing here yet.");
}
1

The scene is set through the application's own functions, as Ada, so the rows the test starts from were made the way the app makes them.

2

The same principal, now in the browser. From here the page is hers, and every read behind it is her read.

3

A control is found by its label, the way a person finds it.

4

The empty state is gone: the assertion that catches a list which appends without clearing.

The same principal and the same application continue through the screen. The test that checks a rule and the test that fills in a form are one kind of thing, in one file.

05

When appearance matters, the same test runs in a real browser

Every assertion so far is about text or state, and a screen can satisfy all of them and still be unusable. osy test renders the page without a compositor, which is cheap and enough for what a person can read and press. osy test --pixels runs the same file in a real browser, where geometry can be judged.

A checkout page. A promo ribbon ships, positioned over the card. The test is three assertions long, and it is the test you would have written.

void the_confirm_button_is_there_and_a_person_can_press_it() {
  Ui.Visit("/");

  Assert.Visible("Confirm");        // text: on the page
  Assert.Enabled("Confirm");        // state: not disabled
  Assert.Clickable("Confirm");      // geometry: a press actually reaches it
}
$ osy test
    │ Assert.Clickable(…) — NOT CHECKED: this run renders in happy-dom, which has no
      compositor — every box it reports is 0×0, so nothing here can judge what covers
      what. Run the same tests with `osy test --pixels` to check them in a real browser.
  ✓ the_confirm_button_is_there_and_a_person_can_press_it (1361ms)

✓ 1 passed.

Green, and honest about it. The claim this renderer could not judge is named in the run as NOT CHECKED, which is a different answer from passed.

$ osy test --pixels
  ✗ the_confirm_button_is_there_and_a_person_can_press_it (2089ms) — at line 7:3
    Assertion failed: Assert.Clickable(…) failed: a click at 'Confirm's own centre
    (640, 140) lands on <div> "half price today" instead — something is covering it.
    That is what a person pressing it would hit, so the control is on screen and
    unusable.

✗ 0 passed, 1 failed.
  📷 1 screenshot(s) — one per UI test, with that test's own data — in osy-shots/

Same file, same three assertions, real Chromium. The button renders, is enabled and is correctly labelled, and nobody can press it.

A pixel run also photographs every page it drove, with that test's own data, into osy-shots/, and prints the path. An agent that wrote the page can open the PNG and look at what it built, which is a different kind of evidence from a passing assertion: an assertion checks the thing you thought to check, and a picture shows the thing you did not.

the last line of a --pixels run
  📷 3 screenshot(s) — one per UI test, with that test's own data — in
     /Users/you/projects/shop/osy-shots

Measured: an agent read this line, listed the directory, opened one of the PNGs and reported what it saw — "labeled fields, the accent color on the primary action, a status badge, and a working switch". Nobody told it to.

Clickability is one geometry question. The browser tier also answers whether a label fits its box, whether a card stays inside its column, and where things sit relative to each other; the whole set, and what the tier needs installed, is in the testing reference. The model is one file: osy test for behaviour, --pixels when geometry matters, and a screenshot when you want to look.

06

A test can move time

The workflows of chapters 03 and 04 wait: an approval that somebody claimed has eight hours before it escalates, and its assignee is nudged halfway. A test does not wait with them. TestClock.Advance moves the application's clock, and every place the application reads the time reads the new one: a function body, the instant bound into a query, and the browser. It is test-only; the compiler refuses it in application code.

demo/wf-approvals/tests/po_approval.test.osyverbatim — this file compiles
void a_claimed_slot_left_to_rot_escalates() {
  var acct = Account.Single(a => a.Owner.Name == "Otto");
  var bea  = User.Single(u => u.Name == "Bea");
1  var po = new PurchaseOrder { Title = "Forklift", Total = 80000, Requester = bea, Account = acct, Category = PoCategory.Capex };
  Workflow.Settle(po);
  PoApproval.RaiseSubmit(po);
2  runas (Lena) { PoApproval.For(po).Legal.Claim(); }

3  TestClock.Advance(TimeSpan.FromHours(9));                  // past Finished.Within = 8h
  Workflow.Settle(po);

4  Assert.Equal(PoStatus.Escalated, po.Status);
}
1

A purchase order from Bea. Its workflow gives a claimed approval eight hours (Finished { Within = TimeSpan.FromHours(8); … }) and goes to Escalated when they run out.

2

Lena claims the legal approval, which starts the eight-hour clock.

3

Nine hours pass. Nothing waits: the run's clock moves, and with it every clock the application reads. Workflow.Settle then runs whatever the workflow has due, the way the platform's own sweep would.

4

The application is now in the state it would be in by tonight, and the test asks about it in the ordinary way.

Time is part of the state a test controls, like the rows the fixture made and the person the test runs as.

What happens when you change it

Testing changes, and what they cost

Each of these is a thing you would do on a Tuesday. The verdict is the compiler's, not a convention's.

You do thisVerdictBecause
Rename an entity field a test readsrefusedThe test is Osy# against your model, so a stale name is a compile error, found before anything runs.
Tighten a security rulecompilesThe tests that run as each side of it say which side moved. The owner's test says whether you went too far, which is the failure a refusal test cannot see.
Add a layout assertion to an existing testcompilesIt runs in both tiers. Under osy test it reports NOT CHECKED; under --pixels it is judged.
Read, in one test, a row another test wrotecompiles…and the row is not there. Each test starts from its own copy of the fixture's state, so there is nothing another test wrote to read, and no order to get right.