glacier-web-cli 0.5.0

Command-line tool for creating and running Glacier applications.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
---
title: Testing Glacier Applications
description: Writing tests in .glacier; the per-test database and the browser-like client; factories, assertions and running the tests.
category: Going Further
order: 1
---

Glacier apps are tested from the start. The model, controller, scaffold
and authentication generators write tests next to the code they make, and
the tests are written in `.glacier` (or in Rust, with `--dialect rust`), so
they read like the controllers and models they check. Each test runs against the real app with a fresh database and
a client that behaves like a browser.

After reading this guide, you will know:

- Where tests live and how they're registered.
- How to write tests, group them and share setup and helpers.
- How the test client sends requests, keeps cookies and handles CSRF.
- How to make records with factories and sequences, and check validation
  errors.
- How to run tests and read their failures.

The examples come from `accounts`, an app made with `glacier new`,
`glacier generate authentication` and a `Note` scaffold whose notes belong
to the user who wrote them. The [Authentication](authentication.html)
guide builds the same app.

## Test Files

Tests live in the app's `tests/` folder, in the same layout as `src/`:

| Path | What |
| --- | --- |
| `tests/controllers/notes_controller.test.glacier` | Requests to `/notes` |
| `tests/models/note.test.glacier` | The `Note` model |
| `tests/factories/note.glacier` | Valid fields for making a `Note` in tests |
| `tests/glacier.rs` | The list of all of them |

A file ending `.test.glacier` holds tests; other `.glacier` files under
`tests/`, such as factories, hold support code. The app's `build.rs`
compiles each of them to Rust, as it does the files in `src/`.

Cargo only looks for tests in `tests/*.rs`, so one file,
`tests/glacier.rs`, lists the others with `glacier::tests!`. The
generators add each file they write to it:

```rust
//! The app's `.glacier` tests and factories (`tests/**/*.glacier`),
//! compiled into one test binary; every `.glacier` file under tests/ is found.
//! Run them with `glacier test` or `cargo test`.

glacier::tests!();

/// A test in plain Rust, run against the app like the `.glacier` ones.
#[glacier::test(app)]
async fn shows_the_sign_up_page() {
    let res = glacier::testing::get("/registration/new").await;
    assert_eq!(res.status, 200u16);
    assert!(res.body.contains("Sign up"));
}
```

Each path names a file without its extension, so
`controllers::notes_controller` is
`tests/controllers/notes_controller.test.glacier`. All the tests build
into one test binary, which links much faster than one binary per file,
and each test is named after its folder and file:
`controllers::notes_controller::lists_only_the_user_s_own_notes`.

NOTE: A new test file must be added to `glacier::tests!` before it runs.
A file listed there that doesn't exist is a build error.

## Writing Tests

### Tests and Assertions

A test is a `test "…" do` block. Its name becomes the Rust function name,
so two tests in the same file, or the same `describe`, can't share a
name. Inside, a test makes requests
and asserts on what comes back:

```glacier
test "lists only the user's own notes" do
  user = sign_in()
  insert(Note, title: "Mine", user_id: user.id)
  other = insert(User)
  insert(Note, title: "Theirs", user_id: other.id)

  res = get "/notes"
  assert res.status == :ok
  assert res.body =~ "Mine"
  refute res.body =~ "Theirs"
end
```

There are four kinds of assertion, and `refute` turns each one around:

| Assertion | Passes when |
| --- | --- |
| `assert a == b` | `a` equals `b` |
| `assert a != b` | `a` doesn't equal `b` |
| `assert text =~ part` | `text` contains `part` |
| `assert condition` | `condition` is `true` |

`=~` checks that the text contains the other string; it isn't a regular
expression match. `refute a == b` is the same check as `assert a != b`.

In a test, `!` after a call waits for it and fails the test, at that
line, if it returns an error, so `Note.count()!` is a number.

`?` does the same for a value that's already there, without waiting: on
an `Err` or a `None`, the test fails at that line. In an action, `?`
returns the error; a test has nothing to return it to, so it fails
instead:

```glacier
test "redirects to the note it creates" do
  user = sign_in()
  res = post "/notes", params_for(Note, user_id: user.id), title: "Mine"
  id = res.location.trim_start_matches("/notes/").parse::<i64>()?
  assert Note.find(id)!.title == "Mine"
end
```

Changed to strip `"/note/"` instead, so that the id doesn't parse, the
test fails with the error:

```text
thread 'controllers::notes_controller::redirects_to_the_note_it_creates' (12891996) panicked at tests/controllers/notes_controller.test.glacier:73:64:
called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }
```

Inside a closure (`fn x -> … end`) in a test, `?` keeps its usual
meaning, returning from the closure.

### Pages by Selector (new)

`=~` checks a page's text, markup and all. To check what the page shows,
select its elements with CSS, as Rails' `assert_select` does:

```glacier
test "lists a page of notes, by selector" do
  for i in 1..=25 do
    insert(Note, title: format!("Note {i}"))
  end
  res = get "/notes"
  assert_select(res, "table tbody tr", count: 20)
  assert_select(res, "a[rel=next]", text: "Next")
  refute_select(res, "a[rel=prev]")
end
```

| Assertion | Passes when |
| --- | --- |
| `assert_select(res, "css")` | at least one element matches |
| `assert_select(res, "css", count: 20)` | exactly 20 match |
| `assert_select(res, "css", text: "Next")` | one that matches has `Next` in its text |
| `refute_select(res, "css")` | none matches |

They take a response or any HTML text, and fail showing the start of the
page. `glacier::testing::select_text(res, "li")` returns each match's
text, for checks of your own.

### Snapshots (new)

A snapshot checks a whole value, such as a JSON response, against a copy
saved the first time the test ran:

```glacier
test "exports a note as JSON, as it was" do
  note = insert(Note, title: "Groceries", body: "Milk, eggs")
  res = get note_json_path(note.id)
  assert_snapshot("note", res.json(), redact: [:id, :inserted_at, :updated_at])
end
```

The first run writes the value to `tests/snapshots/`, named after the
test and the snapshot, and passes:

```json
{
  "id": "[id]",
  "title": "Groceries",
  "body": "Milk, eggs",
  "inserted_at": "[inserted_at]",
  "updated_at": "[updated_at]"
}
```

Commit the file: it's the expected value. A later run whose value differs
fails, showing the lines that changed (`-` saved, `+` now). When the
change is right, run the tests with `GLACIER_UPDATE_SNAPSHOTS=1` to save
the new values, and review the files' diff before committing. `redact:`
names fields, at any depth, whose values change from run to run, such as
ids and timestamps: they're saved as `"[id]"`. Text works too:
`assert_snapshot("page", res.body)`.

### Setup

A `setup do` block runs before each test in its file. It must come before
the tests, and a file has at most one. The generated controller tests use
one to load a form first, for the CSRF token (see below):

```glacier
setup do
  get "/registration/new"
end

test "signs up and signs in" do
  res = post "/registration", [email_address: "new@example.com", password: "password"]
  assert res.status == :see_other
  assert User.count()! == 1
  assert Session.count()! == 1
end

test "rejects a short password" do
  res = post "/registration", [email_address: "new@example.com", password: "short"]
  assert res.status == :unprocessable_entity
  assert res.body =~ "Password is too short"
  assert User.count()! == 0
end

test "refuses a sign-up with the hidden field filled in, as bots do" do
  get "/registration/new"
  res = post "/registration", [email_address: "bot@example.com", password: "password", website: "http://spam.example"]
  assert res.status == :unprocessable_entity
  assert User.count()! == 0
end
```

### Grouping Tests with `describe`

`describe "…" do` groups tests, and may have its own `setup`, which runs
after the file's. Describes don't nest. The group's name is part of each
test's name: `controllers::notes_controller::someone_else_s_note::can_t_be_seen`.

```glacier
describe "someone else's note" do
  setup do
    sign_in()
  end

  test "can't be seen" do
    other = insert(User)
    note = insert(Note, user_id: other.id)
    # Not the user's: as missing as a note that doesn't exist.
    assert get(note).status == :not_found
  end

  test "can't be deleted" do
    other = insert(User)
    note = insert(Note, user_id: other.id)
    assert delete(note).status == :not_found
    assert Note.count()! == 1
  end
end
```

### Helpers

`def` defines a helper at the top of a test file or inside a `describe`.
Helpers can make requests and records as tests do, and calls to them are
awaited for you, so `user = sign_in()` reads like any other call:

```glacier
# Signs in as a new user, as a browser would, and returns them.
def sign_in -> User do
  user = insert(User)
  get "/session/new"
  post "/session", [email_address: &user.email_address, password: "password"]
  user
end
```

In a helper, `?` fails the test as it does in a test, unless the helper
returns a `Result` or an `Option`, in which case `?` returns the error or
`None` to the caller as usual:

```glacier
# The id at the end of a path: 1 for /notes/1.
def id_in(path: &str) -> i64 do
  path.rsplit("/").next()?.parse()?
end

test "finds the new note from its redirect" do
  user = sign_in()
  res = post "/notes", params_for(Note, user_id: user.id)
  assert Note.find(id_in(&res.location))!.user_id == user.id
end
```

### Skipping a Test

`skip:` with a reason keeps a test from running. The test still has to
compile, and `glacier test` lists it as ignored, with the reason:

```glacier
test "links to the notes", skip: "the home page has no menu yet" do
  res = get "/"
  assert res.body =~ "href=\"/notes\""
end
```

## The Database and the Client

Each test gets a fresh, migrated database and its own instance of the app,
so tests don't see each other's records and can run in parallel. With the
default test configuration, `config/test.toml`, the database is an
in-memory SQLite database:

```toml
[database]
url = "sqlite::memory:"
```

Model calls such as `Note.count()` and `insert(User)` use the test's
database, as do the requests the test sends.

### Requests (updated)

`get`, `post`, `patch`, `put` and `delete` send a request to the app,
without opening a socket, and return its response:

- `get` and `delete` take only a path.
- `post`, `patch` and `put` take a path and a form: a keyword list such as
  `[email_address: "user@example.com", password: "password"]`, or a form
  such as `params_for(Note)` followed by fields to set on it.

A keyword list inside another nests its fields under its key:
`[post: [title: "Hi"]]` sends `post[title]=Hi`. A list sends each value
under the key with `[]`, as a form's checkboxes do, which
`params.list("scopes")` reads back:

```glacier
res = post "/api_tokens", [name: "ci", scopes: ["notes:read", "notes:write"]]
```

Forms are sent as
`application/x-www-form-urlencoded`, as a browser sends them. A form can
also hold a file, made with
`glacier::testing::file(filename, content_type, bytes)`; it's then
sent as `multipart/form-data`, as a browser uploads it ([File
Uploads](file_uploads.html) shows it in use).

`post_json`, `patch_json`, `put_json` and `delete_json` take a path and JSON text, or a
form, which is sent as the JSON object it describes, and send it as
`application/json`, as an API client does; [JSON
APIs](json_apis.html#sending-json-bodies) shows them in use.

A path can be a string or a record, which goes to the record's own page,
as `redirect note` does in a controller:

```glacier
test "updates a note" do
  user = sign_in()
  note = insert(Note, user_id: user.id)
  res = patch note, params_for(Note, user_id: user.id), title: "Updated"
  assert res.status == :see_other
  assert get(note).body =~ "Updated"
end
```

The response has four fields:

| Field | What |
| --- | --- |
| `res.status` | The status, compared by name (`:see_other`) or number (`303`) |
| `res.body` | The body, as text |
| `res.location` | The `Location` header, or `""` when there isn't one |
| `res.headers` | All the headers |

Status names are the HTTP reason phrases in snake case: `:ok`,
`:created`, `:see_other`, `:unprocessable_entity`, `:too_many_requests`.
A name that no status has, such as `:se_other`, fails the test rather
than never matching.

```glacier
test "sends security headers with every page" do
  res = get "/"
  assert res.headers["x-frame-options"] == "SAMEORIGIN"
  assert res.headers["x-content-type-options"] == "nosniff"
end
```

### Cookies, Sessions and CSRF (updated)

The client keeps cookies between requests, as a browser does, so signing
in once signs in the rest of the test:

```glacier
test "sends a signed-out visitor to sign in, and back afterwards" do
  res = get "/notes"
  assert res.status == :see_other
  assert res.location == "/session/new"

  user = insert(User)
  get res.location
  res = post "/session", [email_address: &user.email_address, password: "password"]
  assert res.location == "/notes"
end
```

Here `get res.location` follows the redirect, and the second
`res.location` is the page the visitor first asked for.

CSRF protection works in tests as it does in a browser. When a response
contains a form with a CSRF token, the client remembers the token and
sends it with later `post`, `patch`, `put` and `delete` requests. A test
that submits a form without loading a page that has one first is refused,
as a forged request would be:

```glacier
test "refuses a form sent before any page with its CSRF token" do
  res = post "/session", [email_address: "user@example.com", password: "password"]
  assert res.status == :forbidden
  assert res.body == "Invalid CSRF token"
end
```

That's why the generated controller tests start with a `setup` that loads
a form.

`glacier::testing::set_cookie(name, value)` sends a cookie of your own
with the requests that follow, as a browser that already had it would,
beside the ones the app set:

```glacier
test "a remembered locale is a cookie" do
  glacier::testing::set_cookie("locale", "fr")
  assert get("/").body =~ "Boutique"
end
```

### Request Headers

`glacier::testing::header(name, value)` sends a header with every later
request in the test, replacing any earlier value for it.
`glacier::testing::bearer(token)` sends `Authorization: Bearer <token>`,
for testing an API (see [Building JSON APIs](json_apis.html)). The `api`
app's generated tests start a session directly and send its token:

```glacier
test "signs in, returning a token" do
  insert(User, email_address: "user@example.com")
  res = post "/session", [email_address: "user@example.com", password: "password"]
  assert res.status == :created
  assert res.body =~ "\"token\":"
  assert Session.count()! == 1
end

test "rejects a wrong password" do
  insert(User, email_address: "user@example.com")
  res = post "/session", [email_address: "user@example.com", password: "wrong"]
  assert res.status == :unauthorized
  assert Session.count()! == 0
end

test "signs out the session whose token is sent" do
  user = insert(User, email_address: "user@example.com")
  token = Session.start(user.id, None)!
  glacier::testing::bearer(&token)
  res = delete "/session"
  assert res.status == :no_content
  assert Session.count()! == 0
end
```

`header` sends any other header. Here a test asks for JSON from a page
that needs signing in, which answers a JSON client with `401
Unauthorized` rather than a redirect:

```glacier
test "answers a JSON client that isn't signed in with 401" do
  glacier::testing::header("accept", "application/json")
  res = get "/notes"
  assert res.status == :unauthorized
  assert res.status != :see_other
end
```

### Background Jobs

A test starts no job workers, so a job the app enqueues during a test
waits in the test's database. `enqueued_jobs()!` lists the waiting jobs,
with their names and arguments, and `perform_jobs()!` runs the jobs that
are due, until none is left, and returns how many ran. Test files see the
app's jobs without an `alias`. [Background Jobs](background_jobs.html)
shows both in use.

## Time Travel

Code that depends on the time, such as a loan that becomes overdue, can be
tested by moving the test's clock. `travel` moves it forward and stops it
there:

```glacier
loan = insert(Loan, due_at: 14.days().from_now())
refute loan.overdue()

travel(15.days())
assert loan.overdue()
```

`travel_to` sets it to a moment, given as text or a `Timestamp`:

```glacier
travel_to("2026-10-19T09:00:00Z")
loan = insert(Loan, due_at: 2.hours().from_now())
assert loan.due_on_clock("Europe/London") == "19 Oct 2026, 12:00"
```

| Helper | What it does |
| --- | --- |
| `travel(15.days())` | Moves the clock forward (or back, with a negative length) and stops it |
| `travel_to("2026-10-19T09:00:00Z")` | Moves the clock to that moment and stops it |
| `freeze_time()` | Stops the clock now, so times compare exactly |
| `travel_back()` | Lets the clock run with the real time again |

The clock is the one Glacier reads everywhere: `Timestamp.now()`, the
times records are stamped with, and when jobs are due. Each test starts
with the real time, and moving the clock in one test never affects
another. Code that reads the system clock directly, rather than
`Timestamp.now()`, doesn't see the change.

## Factories

A factory gives a model valid fields to start from, so a test only spells
out what matters to it. The generators write one per model in
`tests/factories/`:

```glacier
# Valid fields for a Note, used by `insert(Note)` and
# `params_for(Note)` in tests. Override any of them per call:
# `insert(Note, title: …)`.

factory Note do
  title: "Example title"
  body: "Example body"
  user_id: insert(User).id
end
```

```glacier
# A user who can sign in with "password", each with their own email
# address (`insert(User, email_address: "ada@example.com")` to choose it).
factory User do
  email_address: format!("user{}@example.com", glacier::testing::sequence())
  password: "password"
end
```

Two calls use them:

- `insert(Note)` builds a `Note` from the factory's fields and saves it,
  as `Note.build(…)` and `save()` would, so its validations and callbacks
  run, and returns the saved record. If the record is invalid, the test
  fails with its errors.
- `params_for(Note)` returns the factory's fields as a form, for a
  request or for `Note.build`/`Note.create`.

Either takes fields to override: `insert(Note, title: "Mine", user_id:
user.id)`.

### Sequences

Some fields must be different in every record, such as a user's email
address, which must be unique. `glacier::testing::sequence()` returns a
number that no earlier call in this test binary returned, so a factory can build a different
value each time. The generated `User` factory above uses it, so every
user it makes has their own address, `user1@example.com`,
`user2@example.com` and so on, and a test can insert as many users as it
needs.

Numbers start at 1 and go up by one with each call. The count is shared
by every test in the test binary, not kept per test, and the tests run in
parallel, so which numbers a test gets depends on the others: check the
value the record got, such as `user.email_address`, rather than expecting
a particular one. Each test binary starts counting at 1 again, so two
test runs at once against the same shared database (see [Testing Against
Postgres](#testing-against-postgres)) can make the same values.

To choose the value, set the field. A field a test sets isn't computed by
the factory (see below), so it takes no number:

```glacier
test "gives each user their own email address, unless the test sets one" do
  ada = insert(User)
  grace = insert(User)
  assert ada.email_address != grace.email_address
  assert ada.email_address.starts_with("user")

  user = insert(User, email_address: "ada@example.com")
  assert user.email_address == "ada@example.com"
end
```

Tests that sign in with an address they know set it, as the generated
ones do with `insert(User, email_address: "user@example.com")`, or use
the user's own, as the `sign_in` helper in [Helpers](#helpers) does with
`email_address: &user.email_address`.

`glacier::testing::sequence()` is an ordinary function, so a test can call
it anywhere, not only in a factory; in a test file, or a factory,
`sequence()` is enough.

### Values in Turn (new)

`cycle([...])` gives the next of a few values each time it runs, from
the first, as FactoryBot's cycling sequences do: `status: cycle([:draft,
:published])` in a factory makes records of each status in turn. Each
place `cycle` is written keeps its own turn, starting again in each
test, so the order is the same however tests run:

```glacier
test "cycle gives each record the next value in turn" do
  statuses = (0..4).map(fn _ -> cycle([:draft, :published]) end).collect::<Array<_>>()
  assert statuses == ["draft", "published", "draft", "published"]
end
```

### Fields a Test Sets Aren't Computed (updated)

A field given to `insert` or `params_for` isn't computed by the
factory. The `Note` factory inserts a user for `user_id`, but
`insert(Note, user_id: user.id)` inserts no extra user.

This applies only to fields inside the parentheses. Fields after the form
are set on the result, after the factory has run:

```glacier
res = post "/notes", params_for(Note, user_id: user.id), title: ""
```

So `params_for(Note), user_id: user.id` still inserts the factory's own
user first. The extra user is easy to miss, but it changes counts, and
with a factory whose unique fields have fixed values it fails
validation. This test checks that `params_for(Note, user_id: user.id)`
inserts no user:

```glacier
test "makes no extra user for a user_id the test gives" do
  user = sign_in()
  post "/notes", params_for(Note, user_id: user.id)
  assert User.count()! == 1
end
```

and written with `user_id` after the form, it fails:

```text
---- controllers::notes_controller::makes_no_extra_user_for_a_user_id_the_test_gives stdout ----

thread 'controllers::notes_controller::makes_no_extra_user_for_a_user_id_the_test_gives' (12893120) panicked at tests/controllers/notes_controller.test.glacier:65:3:
assertion failed: User.count()! == 1
  left:  2
  right: 1
```

### Traits (new)

A trait is a named variant of a factory's record: a few fields on top of
the factory's own. A loan that is overdue or already returned is set up
this way in the library example:

```glacier
factory Loan do
  member_id: insert(Member).id
  book_id: insert(Book).id
  due_at: 14.days().from_now()

  trait :overdue do
    due_at: 2.days().ago()
  end

  trait :returned do
    returned_at: 1.days().ago()
  end

  # One field or two: a trait on one line.
  trait :long, due_at: 28.days().from_now()
end
```

A trait of a field or two fits on one line, as `:long` does:
`trait :long, due_at: 28.days().from_now()`.

A call names traits as atoms, after the model and before any fields:

```glacier
late = insert(Loan, :overdue)
assert late.overdue()

back = insert(Loan, :overdue, :returned)
refute back.overdue()

# A field given in the call still wins over the trait's.
soon = insert(Loan, :overdue, due_at: 1.days().from_now())
refute soon.overdue()
```

The factory's fields go in first, then each named trait's, in the
factory's order, so a trait's field wins over the factory's. A field
given in the call wins over both, and as above, isn't computed. Naming a
trait the factory doesn't have fails the test with `this factory has no
trait :name`. `params_for(Loan, :overdue)` and `insert_for(access, Loan,
:overdue)` take traits the same way.

### After Insert (new)

`after_insert do … end`, after a factory's fields, runs on each record
`insert` makes, with the record as `self`, before the test gets it: to
add records it needs, or load what it points at. The library's comment
factory loads each comment's book:

```glacier
factory Comment do
  body: "A fine book."
  commentable_type: "Book"
  commentable_id: insert(Book).id
  # On each comment `insert` makes, as `self`, before the test gets it.
  after_insert do
    self.load(:commentable)!
  end
end
```

```glacier
test "the factory's after_insert loads the comment's book" do
  comment = insert(Comment)
  assert comment.commentable.type_name() == "Book"
  assert insert_list(Comment, 2).all(fn c -> c.commentable.type_name() == "Book" end)
end
```

It runs for `insert`, `insert_list` and `insert_for`, not for
`params_for`, which saves nothing.

### Several Records at Once (new)

`insert_list(Loan, 3, …)` inserts three, and returns them in a `Vec`.
After the count it takes what `insert` takes, traits and fields, and
each record's factory fields are computed for it, so each loan here has
its own book:

```glacier
member = insert(Member)
loans = insert_list(Loan, 3, :overdue, member_id: member.id)
assert loans.len() == 3
assert loans.iter().all(fn loan -> loan.overdue() end)
assert Loan.where(member_id: member.id).count()! == 3
```

### Reusing a Record with `recycle` (new)

A factory that needs a parent inserts one, so `insert_list(Loan, 3)`
makes three members too. `recycle(member)` hands the factories a record
to use instead, for the rest of the test, as Laravel's `recycle` does:
wherever a factory would `insert(Member)`, it takes that member:

```glacier
ada = insert(Member, name: "Ada")
recycle(ada)
loans = insert_list(Loan, 3)
assert loans.iter().all(fn loan -> loan.member_id == ada.id end)
assert Member.count()! == 1
```

Only the factories' own `insert` calls are affected: a test that writes
`insert(Member)` itself still gets a new member. Recycle a record of each
model you want shared; the last one given for a model wins.

### Made-Up Values with `Fake` (new)

`Fake` makes values that look real, for pages, screenshots and seeds, as
Faker does for Rails and Laravel factories:

```glacier
factory Author do
  name: Fake.name()
  bio: Fake.sentence()
end
```

It has `name`, `first_name`, `last_name`, `email` and `username` (both
unique, the email at `example.com`), `company`, `city`,
`street_address`, `phone` (in a range kept for drama, never a real
number), `url`, `word`, `words(n)`, `sentence`, `paragraph`, `title`,
`number(min, max)`, `boolean` and `one_of([...])`. The values come from
small built-in English lists, in the same order on every run, so a
failing test fails the same way again; assert on what a test sets, not
on what `Fake` made.

### Records for an Access

A model `owned_by :user` or `:org` belongs to a tenant, and a test about
tenants wants records that are someone's. `insert_for(access, Note)` is
`insert` for an [Access](authentication.md#who-a-request-is-for-access): the record is built as
`Note.for(access)` builds one, so its owner column comes from the access,
whatever the factory or the test set, and it's saved under the access, so
its owned parents are checked to be the tenant's too:

```glacier
test "insert_for gives a note to the access's user" do
  ada = UserAccess { user: insert(User) }
  bob = insert(User)
  # The factory's user, and even one given here, give way to Ada's.
  note = insert_for(ada, Note, user_id: bob.id)
  assert note.user_id == ada.user.id
  assert Note.for(ada).count()! == 1
end
```

The factory's own fields are still computed, so the `Note` factory still
inserts a user for `user_id` that the note then doesn't use.

## Model Tests

Model tests use the model as the app does. `errors_on(saved)` takes the
result of `Model.create(…)` that failed validation and gives its error
messages joined with `; `, for an `=~` check:

```glacier
test "saves a valid note" do
  note = insert(Note)
  assert Note.count()! == 1
  assert note.title == "Example title"
end

test "requires title" do
  saved = Note.create(params_for(Note, title: ""))!
  assert errors_on(saved) =~ "title can't be blank"
end
```

A record that failed to `save` has its errors too: `user.errors_on("password")`
lists the messages for one field, as the generated `User` tests check:

```glacier
test "stores only a hash of the password" do
  user = insert(User, email_address: "user@example.com")
  assert user.password_digest.starts_with("$argon2id$")
  assert user.password.is_none()
end

test "signs in with the right password only" do
  insert(User, email_address: "user@example.com")
  assert User.authenticate_by("user@example.com", "password")!.is_some()
  assert User.authenticate_by(" USER@example.com ", "password")!.is_some()
  assert User.authenticate_by("user@example.com", "wrong")!.is_none()
  assert User.authenticate_by("nobody@example.com", "password")!.is_none()
end

test "needs a password of 8 characters or more" do
  user = User.build(params_for(User, password: "short"))
  refute user.save()!
  assert user.errors_on("password") == ["is too short (minimum is 8 characters)"]
end

test "email addresses are unique, whatever their case" do
  insert(User, email_address: "user@example.com")
  user = User.build(params_for(User, email_address: "User@Example.com"))
  refute user.save()!
  assert user.errors_on("email_address") == ["has already been taken"]
end
```

`errors_on(saved)` fails the test if the record was saved.

## Unit Tests in a Module

A plain `module` of functions can hold its own tests, beside the code
they test, as Rust's `#[cfg(test)]` tests sit in the file they test:

```glacier
# Run by `glacier test` with the rest, without the app or a database.
test "has a label for every length" do
  assert label(400) == "long"
  assert label(120) == "medium"
  assert label(10) == "short"
  assert first_or(&[3, 4], 0) == 3
  assert first_or(&Array.new(), "none") == "none"
end
```

They call the module's functions by name, take `assert` and `refute` as
other tests do, and `!` fails the test with its line. They run with the
rest under `glacier test`, but without the app: no requests, factories or
database, so they suit functions of plain values. A test that needs any
of those goes in `tests/`.

## Running Tests

`glacier test` compiles the `.glacier` files, builds the app and runs every
test. Anything after it goes to `cargo test`, so a word picks out the
tests whose names contain it:

```sh
$ glacier test own_notes
```

`cargo test` works as well; `glacier test` adds one thing, which is that
failures point at your `.glacier` files.

### Reading a Failure (updated)

Each assertion reports its source and what it found. Here the test was
changed to look for `"Mime"` instead of `"Mine"`:

```text
---- controllers::notes_controller::lists_only_the_user_s_own_notes stdout ----

thread 'controllers::notes_controller::lists_only_the_user_s_own_notes' (12894171) panicked at tests/controllers/notes_controller.test.glacier:36:3:
assertion failed: res.body =~ "Mime"
  expected to contain: "Mime"
  but was: "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>Notes · accounts</title>\n    <link rel=\"stylesheet\" href=\"/app.css\">\n  </head>\n  <body>\n    <p class=\"flash\">Signed in.</p>\n    <h1>Notes</h1>\n\n<table>\n  <thead>\n    <tr><th>Title</th><th>Body</th><th>User</th><th></th></tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td>Mine</td>\n      <td>Example body</td>\n      <td>1</td>\n      <td>\n        <a href=\"/notes/1\">Show</a>\n        <a href=\"/notes/1/edit\">Edit</a>\n      </td>\n    </tr>\n  </tbody>\n</tabl"… (64 more bytes)
```

Long values are cut after 600 characters. The test's name is in the line
above the failure, and is the thread's name: each test runs on a thread
of its own, named after it, with a 16 MB stack, as an app's nested async
calls can need more than a test thread's default in a debug build.

Under `cargo test`, the same failure points at the Rust that `build.rs`
generated, on the same line:

```text
thread 'controllers::notes_controller::lists_only_the_user_s_own_notes' (12894267) panicked at /Users/georgebuckley/framework/guides/examples/accounts/.glacier/tests/controllers/notes_controller.test.rs:36:3:
assertion failed: res.body =~ "Mime"
```

Each `.glacier` line compiles to the same line of Rust, so the line
number is right either way.

When a request in a test answers `500`, the response says only
"Internal Server Error", so the test prints the error itself, with what
caused it, in the test's output, which `cargo test` shows when the test
fails:

```text
PATCH /books/1/pages/2 answered 500 Internal Server Error: error returned from database: (code: 1) no such table: main.leafs
  caused by: (code: 1) no such table: main.leafs
```

## Before a Commit

`glacier precommit` runs what a commit should pass, stopping at the first
step that fails: `glacier fmt --check`, `cargo fmt --check` for the Rust
files, clippy with warnings as errors, the tests, then `glacier audit`.
The generators format what they write, so a new app passes as it is:

```console
$ glacier precommit
== glacier fmt --check
22 file(s) checked; all formatted
== cargo fmt --check
== clippy
== tests
== audit
src/views/docs/show.html:2: raw-html: `raw(…)` skips escaping: make sure nothing a user wrote reaches it
1 finding to look at (not a failure: each is a place to look)
```

### `glacier audit` (updated)

`glacier audit` reads the app's source for places that often hide a
security problem, as Sobelow does for Phoenix, and prints each with its
file and line. It exits with an error when it finds any, so CI can run
it; in `precommit` its findings are printed but don't fail the run. Each
is a place to look, not proof of a bug:

| Finding | What it looks for |
| --- | --- |
| `raw-html` | `raw(…)`, which skips escaping, in code and templates |
| `sql-string` | `Db.sql` with `format!` or `#{…}`: bind values with `.bind(…)` instead; `order_raw`, `where_sql` or `Expr.raw` built from params or `#{…}` |
| `cache-key` | `cache::fetch` in a controller that reads `access`: if the value is someone's, `cache::fetch_for(access, …)` |
| `castable-key` | `Post.build(params)` (or `update`, `assign`) when `Post` `belongs_to :user`, unless the action sets `user_id` itself just after or takes it out with `params.without("user_id")` |
| `cors-any` | `Cors.any_origin()` |
| `get-changes` | a `get` route to an action like `destroy`, `create`, `update` or `publish` |
| `no-csrf` | a `post`, `put`, `patch` or `delete` route outside any pipeline, in an app that has a `browser` pipeline (the webhook route excepted, as it's signed) |

## Testing Against Postgres

The in-memory SQLite database is new for every test. Any other test
database, such as Postgres, is shared by all the tests, so each test runs
inside a transaction that's rolled back when it ends. Everything the test writes, including through its
requests, is gone afterwards.

Point the tests at Postgres in `config/test.toml`, or with the
`GLACIER__DATABASE__URL` environment variable, which overrides it. Migrate
the database before the first run:

```sh
$ GLACIER__DATABASE__URL=postgres://$USER@localhost/glacier_guides_accounts_test glacier db migrate
migrated  m20261004100420_create_users
migrated  m20261004100421_create_sessions
migrated  m20261004100733_create_notes
$ GLACIER__DATABASE__URL=postgres://$USER@localhost/glacier_guides_accounts_test glacier test
…
test result: ok. 33 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 5.76s
```

Every test also runs any migrations that are missing before it starts.
On an empty database, many tests start migrating at once and get in each
other's way, so most of the first run fails with errors from the
database; migrating first avoids that.

Some things behave differently on a shared database:

- Ids aren't reused. A rolled-back insert still uses up its id, so a test
  can't expect the first record it makes to have id 1.
- Tests run in parallel, each in its own transaction, so a test that
  inserts a row with the same unique value as another running test waits
  for that test to finish. The tests pass, but more slowly.

The same works for an app that shares a database with an existing Rails
app: load that app's `db/structure.sql` into the test database, point
`config/test.toml` at it, and give the Glacier models the fields of the
existing tables (see [Models](models.html)). Glacier doesn't
load `structure.sql` itself.

## Tests in Rust

Tests can be plain Rust too. In the test binary that `glacier::tests!`
declares, `#[glacier::test(app)]` runs an async test against the app with
its own database and client, and the functions in `glacier::testing` are
the ones `.glacier` tests call:

```rust
/// A test in plain Rust, run against the app like the `.glacier` ones.
#[glacier::test(app)]
async fn shows_the_sign_up_page() {
    let res = glacier::testing::get("/registration/new").await;
    assert_eq!(res.status, 200u16);
    assert!(res.body.contains("Sign up"));
}
```