EP 352 · Tour of SQLiteData · Jan 26, 2026 ·Members

Video #352: Tour of SQLiteData: Testing

smart_display

Loading stream…

Video #352: Tour of SQLiteData: Testing

Episode: Video #352 Date: Jan 26, 2026 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep352-tour-of-sqlitedata-testing

Episode thumbnail

Description

SQLiteData is incredibly test-friendly. We will show how to configure a test suite for your data layer, how to seed the database for testing, how to assert against this data as it changes, how to employ expectNoDifference for better debugging over Swift Testing’s #expect macro, and how to control the uuid() function used by SQLite.

Video

Cloudflare Stream video ID: 690d60216fec49970fe897768c1d32fe Local file: video_352_tour-of-sqlitedata-testing.mp4 *(download with --video 352)*

References

Transcript

0:05

We have now shown that previews are not inhibited whatsoever by SQLiteData . All of our feature code works in previews, and SQLiteData has even gone above and beyond to make sure that even some advanced functionality works in previews, such as record sharing.

0:22

The same cannot be said of other persistence frameworks out there. Large 3rd party libraries such as Firebase are notorious for putting strain on Xcode’s ability to preview SwiftUI views, and many times people have no choice but to define those little inert views and extract them to a separate package just so that they can get some preview coverage on them. Stephen

0:45

Now let’s turn our attention to unit tests, which are closely related to previews. Unit tests require that we be able to run our app’s features in complete isolation. Isolation means that the code won’t be running on a simulator or actual device, and so the code paths we are testing can’t ever access the real life APIs that interact with the device. Isolation also means that the code paths we execute during tests should not reach out to the outside world to get data because we most likely will not be able to assert on what that data is in the test.

1:14

This can be surprisingly tricky to get right, but SQLiteData’s SyncEngine has most of the hard work taken care of for you. Each of your tests can use a sync engine that is fully isolated from not only the outside world, but also from every other test running in parallel. This means we can write tests much like we would normally without even thinking about the sync engine, and if we need to test something sync engine specific, like the iCloud sharing behavior, then we have that tool available to us via our mocks.

1:40

Let’s take a look at how this works. Writing our first test

1:46

The most testable aspect of our app right now is the game feature, which is the feature responsible for displaying a single game to the user. It allows the user to add and remove players, increment and decrement scores, sort the players by their scores, and share the game with other iCloud users.

2:00

Our other feature in the app, the games feature, which displays the list of all games at the root of the game, is not testable right now. We decided to cram all of the logic and behavior of that feature directly into the view out of convenience, and sadly that means the only way we can test that feature’s functionality is by running it in a preview or booting up a simulator or device.

3:01

So we will focus on the game feature by creating a new test file and getting the basics of a test into place: import Testing @testable import Scorekeeper struct GameFeatureTests { @Test func basics () async throws { } }

3:12

In this test we would like construct a GameModel because that is the thing that encapsulates the logic and behavior of the feature: @Test func basics () async throws { let model = GameModel(game: <#Game#>) }

3:21

If we could construct this then we would be able to invoke various methods on the model to emulate what the user does and then assert on how the state inside the model changes.

3:28

But, to construct a model we need a game. I guess we could just add our database dependency to the test suite: import Dependencies … struct GameFeatureTests { @Dependency(\.defaultDatabase) var database … }

3:36

And then use that database to fetch a game from the database, use the #require Swift testing macro to require its presence, and then pass it along to the GameModel : @Test func basics() async throws { let game = try await #require( database.read { db in try Game.fetchOne(db) } ) let model = GameModel(game: game) }

4:12

Let’s just run this test to make sure any of this code runs successfully: basics(): Caught error: SQLite error 1: no such table: games - while executing SELECT "games"."id", "games"."title" FROM "games" LIMIT ? basics(): Issue recorded: A blank, in-memory database is being used. To set the database that is used by ‘SQLiteData’ in a test, use a tool like the ‘dependency’ trait from ‘DependenciesTestSupport’: import DependenciesTestSupport @Suite(.dependency(\\.defaultDatabase, try DatabaseQueue(/* ... */))) struct MyTests { // ... }

4:17

Well, unfortunately we actually get two failures. The first lets us know that our query to the database was not successful because for some reason the “games” table could not be found. And the second is telling us why we got the first error: we accessed the defaultDatabase dependency without overriding it, and so we are just using the default, blank, un-migrated database.

4:43

To prepare the database for the test we can leverage a test trait that comes with the DependenciesTestSupport module. We must first add the library DependenciesTestSupport to our test target…

4:51

And import that library: import DependenciesTestSupport

4:56

With that done we get access to a trait that allows us to override dependencies for a specific test or an entire test suite: @Test( .dependencies { } ) func basics() async throws { … }

5:06

Whatever dependencies we set in that trailing closure will be set for each individual test in the suite. So we can bootstrap the database there: @Test( .dependencies { try $0.bootstrapDatabase() } ) func basics() async throws { … }

5:12

However, even with all this work the test still fails: Expectation failed: database.read { db in try Game.fetchOne(db) } → nil

5:18

And that’s because although our database is bootstrapped, it is still empty. Seeding the database for tests is a little different from seeding the database for previews. We prefer to localize the seeding to a specific test suite rather than apply seeds globally for all tests because we wouldn’t want a small change to our seeds to suddenly break dozens or hundreds of tests. Further, tests tend to care about the exact values of IDs of rows since it needs to assert on that data, whereas previews don’t really care. So, let’s add seeding to the dependencies trait: .dependencies { … try $0.defaultDatabase.write { db in try db.seed { } } }

5:59

Because seeding is local to the test, it can’t affect or break any other test in the target.

6:09

And as I just said, seeding in tests is very different from seeding in previews because we want to use explicit IDs everywhere so that we have a fighting chance of asserting against all of the row’s data in tests. So, when seeding some games, we won’t use drafts, which are assigned a random ID when inserted into the database, and instead explicitly specify each game’s ID: Game.Draft(id: UUID(0), title: "Family gin rummy") Game.Draft(id: UUID(1), title: "Weekly poker night") Game.Draft(id: UUID(2), title: "Mahjong with grandma")

6:49

And the same for creating players: Player.Draft(id: UUID(0), gameID: UUID(0), name: "Blob", score: 1) Player.Draft(id: UUID(1), gameID: UUID(0), name: "Blob Sr", score: 3) Player.Draft(id: UUID(2), gameID: UUID(0), name: "Blob Jr", score: 2) Note that we have also opted to create these players with much smaller scores than we did in previews: Player.Draft(gameID: UUID(1), name: "Blob", score: 40) Player.Draft(gameID: UUID(1), name: "Blob Sr", score: 100) Player.Draft(gameID: UUID(1), name: "Blob Jr", score: 67)

7:09

Maybe in previews it was appropriate to much larger scores so that we could make sure the UI handles big numbers correctly. But in tests we would prefer to have the scores much closer together so that we will be able to easily test certain things, such as the players re-ordering when a player’s score increments causing them to move up the ranks. This is another good reason why it’s not appropriate to share seeded values with previews and tests.

7:55

Ok, we are finally in a position to write our test. The database has been bootstrap, some sample data has been seeded for this test suite, and if we run the test we will see it passes, which means we are indeed fetching a game from the database.

8:07

And in fact, at this point it would be best to be as explicit as possible with which game we are fetching: try Game.find(UUID(0)).fetchOne(db)

8:17

And so now we have to decide: what do we want to test?

8:20

Well, the most basic thing we could test right now is that all of the players are loaded, and by default we do sort them by their score, descending. We can use the #expect macro from Swift Testing to do this: #expect( model.rows == [ GameModel.Row( player: Player( id: UUID(1), gameID: UUID(0), name: "Blob sr", score: 3 ) ), GameModel.Row( player: Player( id: UUID(2), gameID: UUID(0), name: "Blob jr", score: 2 ) ), GameModel.Row( player: Player( id: UUID(0), gameID: UUID(0), name: "Blob", score: 1 ) ), ] )

9:12

But we do need to make our types Equatable first: @Table struct Player: Equatable, Identifiable { … } … @Selection struct Row: Equatable { let player: Player var imageData: Data? }

9:30

This now compiles, but the test fails: basics(): Expectation failed: (model.rows → []) == ([ GameModel.Row(player: Player(id: UUID(0), gameID: UUID(0), name: "Blob", score: 1)), GameModel.Row(player: Player(id: UUID(1), gameID: UUID(0), name: "Blob Jr", score: 1)), GameModel.Row(player: Player(id: UUID(2), gameID: UUID(0), name: "Blob Sr", score: 1)), ] → [Scorekeeper.GameModel.Row(player: Scorekeeper.Player(id: 00000000-0000-0000-0000-000000000000, gameID: 00000000-0000-0000-0000-000000000000, name: "Blob", score: 1), imageData: nil), Scorekeeper.GameModel.Row(player: Scorekeeper.Player(id: 00000000-0000-0000-0000-000000000001, gameID: 00000000-0000-0000-0000-000000000000, name: "Blob Jr", score: 1), imageData: nil), Scorekeeper.GameModel.Row(player: Scorekeeper.Player(id: 00000000-0000-0000-0000-000000000002, gameID: 00000000-0000-0000-0000-000000000000, name: "Blob Sr", score: 1), imageData: nil)])`

9:37

The failure message isn’t great, but if we look closely we will see that it seems that model.rows is empty: model.rows → []

9:45

This is happening because we introduced an endpoint on the view model to kick off the query for the feature, which is the task method. So we need to emulate the feature appearing by invoking that method in the test too: await model.task()

9:55

Now when we run tests they still fail, but the failure is even harder to decipher: basics(): Expectation failed: (model.rows → [Scorekeeper.GameModel.Row(player: Scorekeeper.Player(id: 00000000-0000-0000-0000-000000000001, gameID: 00000000-0000-0000-0000-000000000000, name: "Blob Sr", score: 3), imageData: nil), Scorekeeper.GameModel.Row(player: Scorekeeper.Player(id: 00000000-0000-0000-0000-000000000002, gameID: 00000000-0000-0000-0000-000000000000, name: "Blob Jr", score: 2), imageData: nil), Scorekeeper.GameModel.Row(player: Scorekeeper.Player(id: 00000000-0000-0000-0000-000000000000, gameID: 00000000-0000-0000-0000-000000000000, name: "Blob", score: 1), imageData: nil)]) == ([ GameModel.Row(player: Player(id: UUID(1), gameID: UUID(0), name: "Blob sr", score: 3)), GameModel.Row(player: Player(id: UUID(2), gameID: UUID(0), name: "Blob jr", score: 2)), GameModel.Row(player: Player(id: UUID(0), gameID: UUID(0), name: "Blob", score: 1)), ] → [Scorekeeper.GameModel.Row(player: Scorekeeper.Player(id: 00000000-0000-0000-0000-000000000001, gameID: 00000000-0000-0000-0000-000000000000, name: "Blob sr", score: 3), imageData: nil), Scorekeeper.GameModel.Row(player: Scorekeeper.Player(id: 00000000-0000-0000-0000-000000000002, gameID: 00000000-0000-0000-0000-000000000000, name: "Blob jr", score: 2), imageData: nil), Scorekeeper.GameModel.Row(player: Scorekeeper.Player(id: 00000000-0000-0000-0000-000000000000, gameID: 00000000-0000-0000-0000-000000000000, name: "Blob", score: 1), imageData: nil)])

10:08

Rather than trying to make heads or tails of this failure, let’s do something better. We have a tool in one of our libraries that makes these kinds of messages a breeze to understand. Let’s go to the Xcode project settings and add a dependency on our Custom Dump library and depend on it in our test target…

10:28

And we’ll import that library into our test: import CustomDump

10:32

And now we will use the library’s expectNoDifference function, which takes two arguments to compare, rather than the #expect macro: expectNoDifference( model.rows, [ GameModel.Row( player: Player( id: UUID(1), gameID: UUID(0), name: "Blob sr", score: 3 ) ), GameModel.Row( player: Player( id: UUID(2), gameID: UUID(0), name: "Blob jr", score: 2 ) ), GameModel.Row( player: Player( id: UUID(0), gameID: UUID(0), name: "Blob", score: 1 ) ), ] )

10:45

This test still fails, but now it’s actually understandable what is wrong: basics(): Issue recorded: Difference: … [ [0]: GameModel.Row( player: Player( id: UUID(00000000-0000-0000-0000-000000000001), gameID: UUID(00000000-0000-0000-0000-000000000000), − name: "Blob Sr", + name: "Blob sr", score: 3 ), imageData: nil ), [1]: GameModel.Row( player: Player( id: UUID(00000000-0000-0000-0000-000000000002), gameID: UUID(00000000-0000-0000-0000-000000000000), − name: "Blob Jr", + name: "Blob jr", score: 2 ), imageData: nil ), [2]: GameModel.Row(…) ] (First: −, Second: +)

10:52

I seem to have accidentally lowercased the “Sr” and “Jr” on the player names. That’s easy enough to fix: expectNoDifference( model.rows, [ GameModel.Row( player: Player( id: UUID(1), gameID: UUID(0), name: "Blob Sr", score: 3 ) ), GameModel.Row( player: Player( id: UUID(2), gameID: UUID(0), name: "Blob Jr", score: 2 ) ), GameModel.Row( player: Player( id: UUID(0), gameID: UUID(0), name: "Blob", score: 1 ) ), ] )

11:05

And now the test passes! Test for adding a player

11:09

Ok, it took a little bit of work, but we now have our first test in place. It isn’t testing a ton, but we at least now know that when the feature first appears it will load all the players for a game, and will sort those players by their score. It’s a start! Brandon

11:21

Let’s now write a new test. Let’s emulate the flow of the user adding a new player to the game. This will confirm that the query we wrote for inserting a new player into the database works correctly, and it will force us to come face-to-face with an uncontrolled dependency lurking in our code.

11:38

Let’s take a look.

11:40

Let’s add a new test function for adding a player: @Test func addPlayer() async throws { }

11:47

We will need a lot of the set up from our other test, such as fetching a particular game, constructing the model, and awaiting the task method: let game = try await #require( database.read { db in try Game.find(UUID(0)).fetchOne(db) } ) let model = GameModel(game: game) await model.task()

12:00

It’s a little annoying to have to copy-and-paste this code into each test, and so we will soon see a better way.

12:02

We also need to bootstrap and seed the database, which we could copy and paste yet again, but this is definitely not ideal. Instead, we could move this work into a shared suite and remove that work from the individual tests: @Suite( .dependencies { try $0.bootstrapDatabase() try $0.defaultDatabase.write { db in try db.seed { … } } } )

12:44

But even better would be to move the bootstrapping to a shared base suite, so that we don’t have to repeat that work in every suite, and then we can localize the seeding to each suite that inherits from it. import DependenciesTestSupport import Testing @testable import Scorekeeper @Suite( .dependencies { try $0.bootstrapDatabase() } ) struct BaseSuite { }

13:43

And by putting our test inside the BaseSuite type it will automatically have all traits applied: extension BaseSuite { @Suite( .dependencies { try $0.defaultDatabase.write { db in try db.seed { … } } } ) struct GameFeatureTests { … } }

16:13

With our suites set up we can now we can emulate the user adding a new player. The way this works in the SwiftUI view is that the user taps the “Add player” button: model.addPlayerButtonTapped()

16:28

Then they write to the newPlayerName state via a SwiftUI binding in a text field: model.newPlayerName = "Blob Esq"

16:40

And then the user taps the “Save player” button: model.saveNewPlayerButtonTapped()

16:47

Once that is done we expect a row to be added to the end of the list. I suppose one way to do this would be like so: expectNoDifference( model.rows.last, GameModel.Row( player: Player( id: UUID(3), gameID: UUID(0), name: "Blob Esq", score: 0 ) ) ) And I’m not sure what ID to choose for the player, but perhaps it should be UUID(3) since that’s the next one after all the other ones that have been generated.

17:42

But this isn’t as strong of an assertion as it could be. If we had a bug that accidentally deleted all of the existing players and then added the new player, this test would still pass.

17:56

The only way to be really sure that the rows contains the 4 players we expect, is to assert on all of that state: expectNoDifference( model.rows, [ GameModel.Row( player: Player( id: UUID(1), gameID: UUID(0), name: "Blob Sr", score: 3 ) ), GameModel.Row( player: Player( id: UUID(2), gameID: UUID(0), name: "Blob Jr", score: 2 ) ), GameModel.Row( player: Player( id: UUID(0), gameID: UUID(0), name: "Blob", score: 1 ) ), GameModel.Row( player: Player( id: UUID(3), gameID: UUID(0), name: "Blob Esq", score: 0 ) ), ] )

18:19

This is of course super verbose, and we will have a much better way to do this soon, but let’s go with it for now.

18:25

If we run this test it fails: addPlayer(): Issue recorded: Difference: … [ … (3 unchanged), + [3]: GameModel.Row( + player: Player( + id: UUID(00000000-0000-0000-0000-000000000003), + gameID: UUID(00000000-0000-0000-0000-000000000000), + name: "Blob Esq", + score: 0 + ), + imageData: nil + ) ] (First: −, Second: +)

18:29

It seems to not have observed the additional row. The reason this is happening is because while adding the player immediately hits the database, the model’s rows are updated in the background shortly after. So we need to wait a small amount of time for the rows to load, which we can do explicitly through the projected value of @FetchAll : try await model.$rows.load()

19:17

If we run this test it still fails, but only barely : addPlayer(): Issue recorded: Difference: … [ … (3 unchanged), [3]: GameModel.Row( player: Player( − id: UUID(B6F46AA8-999F-48EF-8136-22E29A1E25F2), + id: UUID(00000000-0000-0000-0000-000000000003), gameID: UUID(00000000-0000-0000-0000-000000000000), name: "Blob Esq", score: 0 ), imageData: nil ) ] (First: −, Second: +)

19:22

Only the ID of the new player is wrong in our assertion, and it’s wrong because somehow a random UUID has been created.

19:44

The problem is that when we insert a new player into the database, we use a draft, which means its ID is not specified: func saveNewPlayerButtonTapped() { withErrorReporting { try database.write { db in try Player.insert { Player.Draft(gameID: game.id, name: newPlayerName) } .execute(db) } } }

19:56

That causes SQLite to create a new ID for the player, and by default it does so using the uuid() SQLite function: "id" TEXT PRIMARY KEY NOT NULL ON CONFLICT REPLACE DEFAULT (uuid()),

20:07

And it’s that SQLite function that is created a fresh, random UUID. We need to figure out how to control that UUID function so that in tests is generates something we can predict so that we can assert against all of the state in our features.

20:23

Well, amazingly, we can override SQLite’s uuid function with our own Swift function that uses the uuid dependency under the hood. This will allow us to swap out the real UUID generator for an auto-incrementing one in tests.

20:36

To do this we can define a computed property that simply uses @Dependency(\.uuid) to generate a new UUID instead of reaching out to the global, uncontrolled UUID initializer: var uuid: UUID { @Dependency(\.uuid) var uuid return uuid() }

20:53

And we further annotate this property with the @DatabaseFunction macro to generate a bunch of code that makes it easy to invoke this as a function from SQLite: @DatabaseFunction var uuid: UUID { … }

21:11

But, because we are in a default main actor isolation target right now, we do have an error: Main actor-isolated var ‘uuid’ can not be referenced from a nonisolated context

21:25

And the fix is just to mark our computed property as nonisolated : @DatabaseFunction nonisolated var uuid: UUID { @Dependency(\.uuid) var uuid return uuid() }

21:33

And finally we have to register our new database function when we prepare our database: configuration.prepareDatabase { db in db.add(function: $uuid) … }

22:00

This is a big step towards controlling the dependency, but now when we run tests we get a new kind of failure: UUID.swift:65 addPlayer(): Issue recorded: @Dependency(.uuid) has no test implementation, but was accessed from a test context: Location: Scorekeeper/Schema.swift:115 Key: DependencyValues.UUIDGeneratorKey Value: UUIDGenerator Dependencies registered with the library are not allowed to use their default, live implementations when run from tests. To fix, override ‘uuid’ with a test value. If you are using the Composable Architecture, mutate the ‘dependencies’ property on your ‘TestStore’. Otherwise, use ‘withDependencies’ to define a scope for the override. If you’d like to provide a default value for all tests, implement the ‘testValue’ requirement of the ‘DependencyKey’ protocol.

22:15

This is letting us know that we are accessing a dependency in tests without overriding it, and the library does not allow this. You need to override dependencies that are used, and we can do this in our base suite so that all tests use the auto-incrementing UUID generator: @Suite( .dependencies { $0.uuid = .incrementing try $0.bootstrapDatabase() } ) struct BaseSuite { }

23:13

Now when we run tests it still fails, but this time in a strange way: GameFeature.swift:79 addPlayer(): Caught error: SQLite error 19: UNIQUE constraint failed: players.id - while executing INSERT INTO "players" ("id", "gameID", "name", "score")

VALUES 23:23

Somehow, when this line of the test executes: model.saveNewPlayerButtonTapped() …the SQL under the hood is raising a “

UNIQUE 23:35

This is happening because our seeds have already squatted on the first few IDs: @Suite( .dependencies { try $0.defaultDatabase.write { db in try db.seed { Game.Draft( id: UUID(0), title: "Family gin rummy" ) Game.Draft( id: UUID(1), title: "Weekly poker night" ) Game.Draft( id: UUID(2), title: "Mahjong with grandma" ) Player.Draft( id: UUID(0), gameID: UUID(0), name: "Blob", score: 1 ) Player.Draft( id: UUID(1), gameID: UUID(0), name: "Blob Sr", score: 3 ) Player.Draft( id: UUID(2), gameID: UUID(0), name: "Blob Jr", score: 2 ) } } } )

UNIQUE 23:44

…but the UUID generator dependency doesn’t know that, and so when a new row is inserted it happily generates the first UUID(0) again, thus causing the unique constraint failure.

UNIQUE 23:57

So it seems that we can’t specify these IDs explicitly like this. And we did say previously that in tests it is important to know what the IDs are since we have to assert against them, but now that we have controlled our UUID dependency, there is another way we can approach this.

UNIQUE 24:14

We can go back to not specifying the IDs for our seeds: @Suite( .dependencies { try $0.defaultDatabase.write { db in try db.seed { Game.Draft(title: "Family gin rummy") Game.Draft(title: "Weekly poker night") Game.Draft(title: "Mahjong with grandma") Player.Draft(gameID: UUID(0), name: "Blob", score: 1) Player.Draft(gameID: UUID(0), name: "Blob Sr", score: 3) Player.Draft(gameID: UUID(0), name: "Blob Jr", score: 2) } } } )

UNIQUE 24:21

…but because the UUID dependency is now controlled we know that the first game has ID 0, the next has ID 1, and so on.

UNIQUE 24:31

Now when we run tests we get a failure, but it’s only because our IDs are not correct in the assertions. They have rejiggered a little bit: addPlayer(): Issue recorded: Difference: … [ [0]: GameModel.Row( player: Player( − id: UUID(00000000-0000-0000-0000-000000000004), + id: UUID(00000000-0000-0000-0000-000000000001), gameID: UUID(00000000-0000-0000-0000-000000000000), name: "Blob Sr", score: 3 ), imageData: nil ), [1]: GameModel.Row( player: Player( − id: UUID(00000000-0000-0000-0000-000000000005), + id: UUID(00000000-0000-0000-0000-000000000002), gameID: UUID(00000000-0000-0000-0000-000000000000), name: "Blob Jr", score: 2 ), imageData: nil ), [2]: GameModel.Row( player: Player( − id: UUID(00000000-0000-0000-0000-000000000003), + id: UUID(00000000-0000-0000-0000-000000000000), gameID: UUID(00000000-0000-0000-0000-000000000000), name: "Blob", score: 1 ), imageData: nil ), [3]: GameModel.Row( player: Player( − id: UUID(00000000-0000-0000-0000-000000000006), + id: UUID(00000000-0000-0000-0000-000000000003), gameID: UUID(00000000-0000-0000-0000-000000000000), name: "Blob Esq", score: 0 ), imageData: nil ) ] (First: −, Second: +)

UNIQUE 24:52

So if we fix that: expectNoDifference( model.rows, [ GameModel.Row( player: Player( id: UUID(4), gameID: UUID(0), name: "Blob Sr", score: 3 ) ), GameModel.Row( player: Player( id: UUID(5), gameID: UUID(0), name: "Blob Jr", score: 2 ) ), GameModel.Row( player: Player( id: UUID(3), gameID: UUID(0), name: "Blob", score: 1 ) ), GameModel.Row( player: Player( id: UUID(6), gameID: UUID(0), name: "Blob Esq", score: 0 ) ), ] )

UNIQUE 25:06

…the test now passes and we have tests the flow of the user adding a player to their game.

UNIQUE 25:16

But of course, it’s also kind of a pain to have to manage IDs like this. The worst part is that if we did something seemingly innocuous such as re-order our seeds: @Suite( .dependencies { try $0.defaultDatabase.write { db in try db.seed { Game.Draft(title: "Family gin rummy") Player.Draft(gameID: UUID(0), name: "Blob", score: 1) Player.Draft(gameID: UUID(0), name: "Blob Sr", score: 3) Player.Draft(gameID: UUID(0), name: "Blob Jr", score: 2) Game.Draft(title: "Weekly poker night") Game.Draft(title: "Mahjong with grandma") } } } )

UNIQUE 25:34

…we have now secretly changed the IDs of our players and so now our tests fail.

UNIQUE 25:50

Luckily there is a better way. We can demarcate IDs that are only used for seeding so that those IDs never overlap with the ones generated by the UUID dependency. We can do this with negative numbers: try db.seed { Game.Draft(id: UUID(-1), title: "Family gin rummy") Game.Draft(id: UUID(-2), title: "Weekly poker night") Game.Draft(id: UUID(-3), title: "Mahjong with grandma") Player.Draft( id: UUID(-1), gameID: UUID(-1), name: "Blob", score: 1 ) Player.Draft( id: UUID(-2), gameID: UUID(-1), name: "Blob Sr", score: 3 ) Player.Draft( id: UUID(-3), gameID: UUID(-1), name: "Blob Jr", score: 2 ) }

UNIQUE 26:20

It may seem weird, but our

UUID 26:41

We just need to update our set up code: try Game.find(UUID(-1)).fetchOne(db) …and our tests to use these new IDs…

UUID 27:32

And now everything passes, and our tests are resilient to changes in our seeds. We can re-order them: try db.seed { Game.Draft(id: UUID(-1), title: "Family gin rummy") Player.Draft( id: UUID(-1), gameID: UUID(-1), name: "Blob", score: 1 ) Player.Draft( id: UUID(-2), gameID: UUID(-1), name: "Blob Sr", score: 3 ) Player.Draft( id: UUID(-3), gameID: UUID(-1), name: "Blob Jr", score: 2 ) Game.Draft(id: UUID(-2), title: "Weekly poker night") Game.Draft(id: UUID(-3), title: "Mahjong with grandma") }

UUID 27:57

…and everything still passes. We can even create all new values in the seeds, such as adding players to a different game: try db.seed { Game.Draft(id: UUID(-1), title: "Family gin rummy") Player.Draft( id: UUID(-1), gameID: UUID(-1), name: "Blob", score: 1 ) Player.Draft( id: UUID(-2), gameID: UUID(-1), name: "Blob Sr", score: 3 ) Player.Draft( id: UUID(-3), gameID: UUID(-1), name: "Blob Jr", score: 2 ) Game.Draft(id: UUID(-2), title: "Weekly poker night") Player.Draft( id: UUID(-4), gameID: UUID(-2), name: "Brandon", score: 1 ) Player.Draft( id: UUID(-5), gameID: UUID(-2), name: "Stephen", score: 3 ) Game.Draft(id: UUID(-3), title: "Mahjong with grandma") }

UUID 28:15

…and still all tests pass. And we will soon be releasing a dedicated tool for bucketing these kinds of IDs that can make very large test suites even more resilient to seeding changes.

UUID 28:40

So tests are passing, and if we ever had a logic error in our feature, such as if when creating the player we forgot to include the player’s name because it’s optional: try Player.insert { Player.Draft(gameID: game.id) } .execute(db)

UUID 28:56

Then running our test instantly shows us that something is not right: addPlayer(): Issue recorded: Difference: … [ … (3 unchanged), [3]: GameModel.Row( player: Player( id: UUID(00000000-0000-0000-0000-000000000006), gameID: UUID(00000000-0000-0000-0000-000000000000), − name: "", + name: "Blob Esq", score: 0 ), imageData: nil ) ] (First: −, Second: +)

UUID 29:03

Now the changes we have just made has caused our basics() test to start failing because its IDs are no longer correct. But that is easy enough to fix: expectNoDifference( model.rows, [ GameModel.Row( player: Player( id: UUID(4), gameID: UUID(0), name: "Blob Sr", score: 3 ) ), GameModel.Row( player: Player( id: UUID(5), gameID: UUID(0), name: "Blob Jr", score: 2 ) ), GameModel.Row( player: Player( id: UUID(3), gameID: UUID(0), name: "Blob", score: 1 ) ), ] )

UUID 29:15

And now the whole test suite is passing. Next time: Test improvements

UUID 29:18

Ok, we have now exercise more of our feature’s logic and behavior in a simple unit test. We can emulate the user tapping on various buttons and typing into text fields to add a new player to their game, and then we assert that indeed the game was added to the model’s state. We can now be very confident that this feature works when run in the simulator or on device, as long as the view is reading the state from the model to construct its hierarchy and as long as the view is calling the model’s methods when the user takes an action. Stephen

UUID 29:50

Things are looking good, but there are some improvements we can make to this. We had to copy-and-paste quite a bit of set up code in that test just to get to a point where we could actually start emulating user behavior and making assertions. And the assertion itself was quite verbose. We are going to make both of those aspects much better, and we are going to take a look at how we can use AI to write a few more tests for us.

UUID 30:12

Let’s take a look…next time! References SQLiteData Brandon Williams & Stephen Celis A fast, lightweight replacement for SwiftData, powered by SQL. https://github.com/pointfreeco/sqlite-data StructuredQueries A library for building SQL in a safe, expressive, and composable manner. https://github.com/pointfreeco/swift-structured-queries Downloads Sample code 0352-sqlite-data-tour-pt6 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .