Skip to content

Commit d2bc770

Browse files
committed
feat: add test tools docs
1 parent ea118fb commit d2bc770

File tree

5 files changed

+550
-0
lines changed

5 files changed

+550
-0
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Get started
2+
3+
Serverpod provides simple but feature rich test tools to make testing your backend a breeze.
4+
5+
<details>
6+
<summary> Have an existing project? Follow these steps first!</summary>
7+
<p>
8+
For existing projects, a few extra things need to be done:
9+
1. Add the `server_test_tools_path` key to `config/generator.yaml`. Without this key, the test tools file is not generated. The default location for the generated file is `integration_test/test_tools/serverpod_test_tools.dart`, but this can be set to any path (though should be outside of `lib` as per Dart's test conventions).
10+
11+
2. New projects now come with a test profile in `docker-compose.yaml`. This is not strictly mandatory, but is recommended to ensure that the testing state is never polluted. Add the snippet below to the `docker-compose.yaml` file in the server directory:
12+
13+
```yaml
14+
# Test services
15+
postgres_test:
16+
image: postgres:16.3
17+
ports:
18+
- '9090:5432'
19+
environment:
20+
POSTGRES_USER: postgres_test
21+
POSTGRES_DB: projectname_test
22+
POSTGRES_PASSWORD: "<insert database test password>"
23+
volumes:
24+
- projectname_data:/var/lib/postgresql/data
25+
profiles:
26+
- '' # Default profile
27+
- test
28+
redis_test:
29+
image: redis:6.2.6
30+
ports:
31+
- '9091:6379'
32+
command: redis-server --requirepass "<insert redis test password>"
33+
environment:
34+
- REDIS_REPLICATION_MODE=master
35+
profiles:
36+
- '' # Default profile
37+
- test
38+
```
39+
40+
3. Create a `test.yaml` file and add it to the `config` directory:
41+
42+
```yaml
43+
# This is the configuration file for your local test environment. By
44+
# default, it runs a single server on port 8090. To set up your server, you will
45+
# need to add the name of the database you are connecting to and the user name.
46+
# The password for the database is stored in the config/passwords.yaml.
47+
#
48+
# When running your server locally, the server ports are the same as the public
49+
# facing ports.
50+
51+
# Configuration for the main API test server.
52+
apiServer:
53+
port: 9080
54+
publicHost: localhost
55+
publicPort: 9080
56+
publicScheme: http
57+
58+
# Configuration for the Insights test server.
59+
insightsServer:
60+
port: 9081
61+
publicHost: localhost
62+
publicPort: 9081
63+
publicScheme: http
64+
65+
# Configuration for the web test server.
66+
webServer:
67+
port: 9082
68+
publicHost: localhost
69+
publicPort: 9082
70+
publicScheme: http
71+
72+
# This is the database setup for your test server.
73+
database:
74+
host: localhost
75+
port: 9090
76+
name: projectname_test
77+
user: postgres
78+
79+
# This is the setup for your Redis test instance.
80+
redis:
81+
enabled: false
82+
host: localhost
83+
port: 9091
84+
```
85+
86+
4. Add this entry to `config/passwords.yaml`
87+
88+
```yaml
89+
test:
90+
database: '<insert database test password>'
91+
redis: '<insert redis test password>'
92+
```
93+
94+
That's it, the project setup should be ready to start using the test tools!
95+
</p>
96+
</details>
97+
98+
Go to the server directory and generate the test tools by running `serverpod generate`. The default location for the generated file is `integration_test/test_tools/serverpod_test_tools.dart`.
99+
100+
The generated file exports a `withServerpod` helper that enables you to call your endpoints directly like regular functions:
101+
102+
```dart
103+
// Import the generated file, it contains everything you need.
104+
import 'test_tools/serverpod_test_tools.dart';
105+
106+
void main() {
107+
withServerpod('Given Example endpoint', (sessionBuilder, endpoints) {
108+
test('when calling `hello` then should return greeting', () async {
109+
final greeting =
110+
await endpoints.example.hello(sessionBuilder, 'Michael');
111+
expect(greeting, 'Hello, Michael!');
112+
});
113+
});
114+
}
115+
```
116+
117+
A few things to note from the above example:
118+
119+
- The test tools should be imported from the generated test tools file and not the `serverpod_test` package.
120+
- The `withServerpod` callback takes two parameters: `sessionBuilder` and `endpoints`.
121+
- `sessionBuilder` is used to build a `session` object that represents the state of the world for your endpoints and is used to set up scenarios.
122+
- `endpoints` contains all your Serverpod endpoints and lets you call them.
123+
124+
:::info
125+
126+
The location of the test tools can be changed by changing the `server_test_tools_path` key in `config/generator.yaml`. If you remove the `server_test_tools_path` key, the test tools will stop being generated.
127+
128+
:::
129+
130+
Before you can run the test you also need to start the Postgres and Redis:
131+
132+
```bash
133+
docker-compose up --build --detach
134+
```
135+
136+
By default this starts up both the `development` and `test` profiles. To only start one profile, simply add `--profile test` to the command.
137+
138+
Now the test is ready to be run!
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# The basics
2+
3+
## Using `sessionBuilder` to set up a test scenario
4+
5+
The `withServerpod` helper provides a `session` object that helps with setting up different scenarios for tests. It looks like the following:
6+
7+
```dart
8+
/// A test specific builder to create a [Session] that for instance can be used to call database methods.
9+
/// The builder can also be passed to endpoint calls. The builder will create a new session for each call.
10+
abstract class TestSessionBuilder {
11+
/// Given the properties set on the session through the `copyWith` method,
12+
/// this returns a serverpod [Session] that has the configured state.
13+
Session build();
14+
15+
/// Creates a new unique session with the provided properties.
16+
/// This is useful for setting up different session states in the tests
17+
/// or simulating multiple users.
18+
TestSessionBuilder copyWith({
19+
AuthenticationOverride? authentication,
20+
bool? enableLogging,
21+
});
22+
}
23+
```
24+
25+
To create a new state, simply call `copyWith` with the new properties and use the new session builder in the endpoint call.
26+
27+
Below follows examples of some common scenarios.
28+
29+
### Setting authenticated state
30+
31+
```dart
32+
withServerpod('Given AuthenticatedExample endpoint',
33+
(sessionBuilder, endpoints) {
34+
// Corresponds to an actual user id
35+
const int userId = 1234;
36+
37+
group('when authenticated', () {
38+
var authenticatedSessionBuilder = sessionBuilder.copyWith(
39+
authentication:
40+
AuthenticationOverride.authenticationInfo(userId, {Scope('user')}),
41+
);
42+
43+
test('then calling `hello` should return greeting', () async {
44+
final greeting = await endpoints.authenticatedExample
45+
.hello(authenticatedSessionBuilder, 'Michael');
46+
expect(greeting, 'Hello, Michael!');
47+
});
48+
});
49+
50+
group('when unauthenticated', () {
51+
var unauthenticatedSessionBuilder = sessionBuilder.copyWith(
52+
authentication: AuthenticationOverride.unauthenticated(),
53+
);
54+
55+
test(
56+
'then calling `hello` should throw `ServerpodUnauthenticatedException`',
57+
() async {
58+
final future = endpoints.authenticatedExample
59+
.hello(unauthenticatedSessionBuilder, 'Michael');
60+
await expectLater(
61+
future, throwsA(isA<ServerpodUnauthenticatedException>()));
62+
});
63+
});
64+
});
65+
```
66+
67+
### Seeding the database
68+
69+
To seed the database before tests, simply `build` a `session` and pass to the database call exactly the same as in production code.
70+
71+
:::info
72+
73+
By default `withServerpod` does all database operations inside a transaction that is rolled back after each `test` case. See the [rollback database configuration](#rollback-database-configuration) for how to configure this behavior.
74+
75+
:::
76+
77+
```dart
78+
withServerpod('Given Products endpoint when authenticated',
79+
(sessionBuilder, endpoints) {
80+
const int userId = 1234;
81+
var authenticatedSession = sessionBuilder
82+
.copyWith(
83+
authentication: AuthenticationOverride.authenticationInfo(
84+
userId,
85+
{Scope('user')},
86+
),
87+
)
88+
.build();
89+
90+
setUp(() async {
91+
await Product.db.insert(authenticatedSession, [
92+
Product(name: 'Apple', price: 10),
93+
Product(name: 'Banana', price: 10)
94+
]);
95+
});
96+
97+
test('then calling `all` should return all products', () async {
98+
final products = await endpoints.products.all(authenticatedSession);
99+
expect(products, hasLength(2));
100+
expect(products.map((p) => p.name), contains(['Apple', 'Banana']));
101+
});
102+
});
103+
```
104+
105+
## Environment
106+
107+
By default `withServerpod` uses the `test` run mode and the database settings will be read from `config/test.yaml`.
108+
109+
It is possible to override the default run mode by setting the `runMode` setting:
110+
111+
```dart
112+
withServerpod(
113+
'Given Products endpoint when authenticated',
114+
(sessionBuilder, endpoints) {
115+
/* test code */
116+
},
117+
runMode: ServerpodRunMode.development,
118+
);
119+
```
120+
121+
## Configuration
122+
123+
The following optional configuration options are available to pass as a second argument to `withServerpod`:
124+
125+
```dart
126+
{
127+
RollbackDatabase? rollbackDatabase = RollbackDatabase.afterEach,
128+
String? runMode = ServerpodRunmode.test,
129+
bool? enableSessionLogging = false,
130+
bool? applyMigrations = true,
131+
}
132+
```
133+
134+
### `rollbackDatabase` {#rollback-database-configuration}
135+
136+
By default `withServerpod` does all database operations inside a transaction that is rolled back after each `test` case. Just like the following enum describes, the behavior of the automatic rollbacks can be configured:
137+
138+
```dart
139+
/// Options for when to rollback the database during the test lifecycle.
140+
enum RollbackDatabase {
141+
/// After each test. This is the default.
142+
afterEach,
143+
144+
/// After all tests.
145+
afterAll,
146+
147+
/// Disable rolling back the database.
148+
disabled,
149+
}
150+
```
151+
152+
There are two main reasons to change the default setting:
153+
154+
1. **Scenario tests**: when consecutive `test` cases depend on each other. While generally considered an anti-pattern, it can be useful when the set up for the test group is very expensive. In this case `rollbackDatabase` can be set to `RollbackDatabase.afterAll` to ensure that the database state persists between `test` cases. At the end of the `withServerpod` scope, all database changes will be rolled back.
155+
156+
2. **Concurrent transactions in endpoints**: when concurrent calls are made to `session.db.transaction` inside an endpoint, it is no longer possible for the Serverpod test tools to do these operations as part of a top level transaction. In this case this feature should be disabled by passing `RollbackDatabase.disabled`.
157+
158+
```dart
159+
Future<void> concurrentTransactionCalls(
160+
Session session,
161+
) async {
162+
await Future.wait([
163+
session.db.transaction((tx) => /*...*/),
164+
// Will throw `InvalidConfigurationException` if `rollbackDatabase`
165+
// is not set to `RollbackDatabase.disabled` in `withServerpod`
166+
session.db.transaction((tx) => /*...*/),
167+
]);
168+
}
169+
```
170+
171+
When setting `rollbackDatabase.disabled` to be able to test `concurrentTransactionCalls`, remember that the database has to be manually cleaned up to not leak data:
172+
173+
```dart
174+
withServerpod(
175+
'Given ProductsEndpoint when calling concurrentTransactionCalls',
176+
(sessionBuilder, endpoints) {
177+
tearDownAll(() async {
178+
var session = sessionBuilder.build();
179+
// If something was saved to the database in the endpoint,
180+
// for example a `Product`, then it has to be cleaned up!
181+
await Product.db.deleteWhere(
182+
session,
183+
where: (_) => Constant.bool(true),
184+
);
185+
});
186+
187+
test('then should execute and commit all transactions', () async {
188+
var result =
189+
await endpoints.products.concurrentTransactionCalls(sessionBuilder);
190+
// ...
191+
});
192+
},
193+
rollbackDatabase: RollbackDatabase.disabled,
194+
);
195+
```
196+
197+
### `runMode`
198+
199+
The run mode that Serverpod should be running in. Defaults to `test`.
200+
201+
### `enableSessionLogging`
202+
203+
Wether session logging should be enabled. Defaults to `false`.
204+
205+
### `applyMigrations`
206+
207+
Wether pending migrations should be applied when starting Serverpod. Defaults to `true`.

0 commit comments

Comments
 (0)