Skip to main content
Version: Next

The basics

Set up a test scenario

The withServerpod helper provides a sessionBuilder that helps with setting up different scenarios for tests. To modify the session builder's properties, call its copyWith method. It takes the following named parameters:

PropertyTypeDefaultDescription
authenticationAuthenticationOverride?AuthenticationOverride.unauthenticated()See section Setting authenticated state.
enableLoggingbool?falseWether logging is turned on for the session.

The copyWith method creates a new unique session builder with the provided properties. This can then be used in endpoint calls (see section Setting authenticated state for an example).

To build out a Session (to use for database calls or pass on to functions), simply call the build method:

Session session = sessionBuilder.build();

Given the properties set on the session builder through the copyWith method, this returns a Serverpod Session that has the corresponding state.

Setting authenticated state

To control the authenticated state of the session, the AuthenticationOverride class can be used.

To create an unauthenticated override (this is the default value for new sessions), call AuthenticationOverride unauthenticated():

static AuthenticationOverride unauthenticated();

To create an authenticated override, call AuthenticationOverride.authenticationInfo(...):

static AuthenticationOverride authenticationInfo(
int userId,
Set<Scope> scopes, {
String? authId,
})

Pass these to sessionBuilder.copyWith to simulate different scenarios. Below follows an example for each case:

withServerpod('Given AuthenticatedExample endpoint', (sessionBuilder, endpoints) {
// Corresponds to an actual user id
const int userId = 1234;

group('when authenticated', () {
var authenticatedSessionBuilder = sessionBuilder.copyWith(
authentication:
AuthenticationOverride.authenticationInfo(userId, {Scope('user')}),
);

test('then calling `hello` should return greeting', () async {
final greeting = await endpoints.authenticatedExample
.hello(authenticatedSessionBuilder, 'Michael');
expect(greeting, 'Hello, Michael!');
});
});

group('when unauthenticated', () {
var unauthenticatedSessionBuilder = sessionBuilder.copyWith(
authentication: AuthenticationOverride.unauthenticated(),
);

test(
'then calling `hello` should throw `ServerpodUnauthenticatedException`',
() async {
final future = endpoints.authenticatedExample
.hello(unauthenticatedSessionBuilder, 'Michael');
await expectLater(
future, throwsA(isA<ServerpodUnauthenticatedException>()));
});
});
});

Seeding the database

To seed the database before tests, build a session and pass it to the database call just as in production code.

info

By default withServerpod does all database operations inside a transaction that is rolled back after each test case. See the rollback database configuration for how to configure this behavior.

withServerpod('Given Products endpoint', (sessionBuilder, endpoints) {
var session = sessionBuilder.build();

setUp(() async {
await Product.db.insert(session, [
Product(name: 'Apple', price: 10),
Product(name: 'Banana', price: 10)
]);
});

test('then calling `all` should return all products', () async {
final products = await endpoints.products.all(sessionBuilder);
expect(products, hasLength(2));
expect(products.map((p) => p.name), contains(['Apple', 'Banana']));
});
});

Environment

By default withServerpod uses the test run mode and the database settings will be read from config/test.yaml.

It is possible to override the default run mode by setting the runMode setting:

withServerpod(
'Given Products endpoint',
(sessionBuilder, endpoints) {
/* test code */
},
runMode: ServerpodRunMode.development,
);

Configuration

The following optional configuration options are available to pass as a second argument to withServerpod:

PropertyTypeDefault
rollbackDatabaseRollbackDatabase?RollbackDatabase.afterEach
runModeString?ServerpodRunmode.test
enableSessionLoggingbool?false
applyMigrationsbool?true
testGroupTagsOverrideList<String>?null

rollbackDatabase

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:

/// Options for when to rollback the database during the test lifecycle.
enum RollbackDatabase {
/// After each test. This is the default.
afterEach,

/// After all tests.
afterAll,

/// Disable rolling back the database.
disabled,
}

There are a few reasons to change the default setting:

  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.

  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.

Future<void> concurrentTransactionCalls(
Session session,
) async {
await Future.wait([
session.db.transaction((tx) => /*...*/),
// Will throw `InvalidConfigurationException` if `rollbackDatabase`
// is not set to `RollbackDatabase.disabled` in `withServerpod`
session.db.transaction((tx) => /*...*/),
]);
}

When setting rollbackDatabase.disabled to be able to test concurrentTransactionCalls, remember that the database has to be manually cleaned up to not leak data:

withServerpod(
'Given ProductsEndpoint when calling concurrentTransactionCalls',
(sessionBuilder, endpoints) {
tearDownAll(() async {
var session = sessionBuilder.build();
// If something was saved to the database in the endpoint,
// for example a `Product`, then it has to be cleaned up!
await Product.db.deleteWhere(
session,
where: (_) => Constant.bool(true),
);
});

test('then should execute and commit all transactions', () async {
var result =
await endpoints.products.concurrentTransactionCalls(sessionBuilder);
// ...
});
},
rollbackDatabase: RollbackDatabase.disabled,
);

Additionally, when setting rollbackDatabase.disabled, it may also be needed to pass the --concurrency=1 flag to the dart test runner. Otherwise multiple tests might pollute each others database state:

dart test -t integration --concurrency=1

For the other cases this is not an issue, as each withServerpod has its own transaction and will therefore be isolated.

  1. Database exceptions that are quelled: There is a specific edge case where the test tools behavior deviates from production behavior. See example below:
var transactionFuture = session.db.transaction((tx) async {
var data = UniqueData(number: 1, email: 'test@test.com');
try {
await UniqueData.db.insertRow(session, data, transaction: tx);
await UniqueData.db.insertRow(session, data, transaction: tx);
} on DatabaseException catch (_) {
// Ignore the database exception
}
});

// ATTENTION: This will throw an exception in production
// but not in the test tools.
await transactionFuture;

In production, the transaction call will throw if any database exception happened during its execution, even if the exception was first caught inside the transaction. However, in the test tools this will not throw an exception due to how the nested transactions are emulated. Quelling exceptions like this is not best practise, but if the code under test does this setting rollbackDatabase to RollbackDatabse.disabled will ensure the code behaves like in production.

runMode

The run mode that Serverpod should be running in. Defaults to test.

enableSessionLogging

Wether session logging should be enabled. Defaults to false.

applyMigrations

Wether pending migrations should be applied when starting Serverpod. Defaults to true.

testGroupTagsOverride

By default Serverpod test tools tags the withServerpod test group with "integration". This is to provide a simple way to only run unit or integration tests. This property allows this tag to be overridden to something else. Defaults to null (i.e. no override).

Test exceptions

The following exceptions are exported from the generated test tools file and can be thrown by the test tools in various scenarios, see below.

ExceptionDescription
ServerpodUnauthenticatedExceptionThrown during an endpoint method call when the user was not authenticated.
ServerpodInsufficientAccessExceptionThrown during an endpoint method call when the authentication key provided did not have sufficient access.
ConnectionClosedExceptionThrown during an endpoint method call if a stream connection was closed with an error. For example, if the user authentication was revoked.
InvalidConfigurationExceptionThrown when an invalid configuration state is found.

Test helpers

flushEventQueue

Test helper to flush the event queue. Useful for waiting for async events to complete before continuing the test.

Future<void> flushEventQueue();

For example, if depending on a generator function to execute up to its yield, then the event queue can be flushed to ensure the generator has executed up to that point:

var stream = endpoints.someEndoint.generatorFunction(session);
await flushEventQueue();

See also this complete example.