From d2bc770e4ccbc66bc140b502cf1c6e296b0b437e Mon Sep 17 00:00:00 2001 From: Hampus Lavin Date: Mon, 7 Oct 2024 18:20:14 +0200 Subject: [PATCH 01/12] feat: add test tools docs --- docs/06-concepts/18-testing/01-get-started.md | 138 ++++++++++++ docs/06-concepts/18-testing/02-the-basics.md | 207 ++++++++++++++++++ .../18-testing/03-advanced-examples.md | 98 +++++++++ .../18-testing/04-best-practises.md | 103 +++++++++ docs/06-concepts/18-testing/_category_.json | 4 + 5 files changed, 550 insertions(+) create mode 100644 docs/06-concepts/18-testing/01-get-started.md create mode 100644 docs/06-concepts/18-testing/02-the-basics.md create mode 100644 docs/06-concepts/18-testing/03-advanced-examples.md create mode 100644 docs/06-concepts/18-testing/04-best-practises.md create mode 100644 docs/06-concepts/18-testing/_category_.json diff --git a/docs/06-concepts/18-testing/01-get-started.md b/docs/06-concepts/18-testing/01-get-started.md new file mode 100644 index 00000000..7c2dcf1f --- /dev/null +++ b/docs/06-concepts/18-testing/01-get-started.md @@ -0,0 +1,138 @@ +# Get started + +Serverpod provides simple but feature rich test tools to make testing your backend a breeze. + +
+ Have an existing project? Follow these steps first! +

+For existing projects, a few extra things need to be done: +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). + +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: + +```yaml +# Test services +postgres_test: + image: postgres:16.3 + ports: + - '9090:5432' + environment: + POSTGRES_USER: postgres_test + POSTGRES_DB: projectname_test + POSTGRES_PASSWORD: "" + volumes: + - projectname_data:/var/lib/postgresql/data + profiles: + - '' # Default profile + - test +redis_test: + image: redis:6.2.6 + ports: + - '9091:6379' + command: redis-server --requirepass "" + environment: + - REDIS_REPLICATION_MODE=master + profiles: + - '' # Default profile + - test +``` + +3. Create a `test.yaml` file and add it to the `config` directory: + +```yaml +# This is the configuration file for your local test environment. By +# default, it runs a single server on port 8090. To set up your server, you will +# need to add the name of the database you are connecting to and the user name. +# The password for the database is stored in the config/passwords.yaml. +# +# When running your server locally, the server ports are the same as the public +# facing ports. + +# Configuration for the main API test server. +apiServer: + port: 9080 + publicHost: localhost + publicPort: 9080 + publicScheme: http + +# Configuration for the Insights test server. +insightsServer: + port: 9081 + publicHost: localhost + publicPort: 9081 + publicScheme: http + +# Configuration for the web test server. +webServer: + port: 9082 + publicHost: localhost + publicPort: 9082 + publicScheme: http + +# This is the database setup for your test server. +database: + host: localhost + port: 9090 + name: projectname_test + user: postgres + +# This is the setup for your Redis test instance. +redis: + enabled: false + host: localhost + port: 9091 +``` + +4. Add this entry to `config/passwords.yaml` + +```yaml +test: + database: '' + redis: '' +``` + +That's it, the project setup should be ready to start using the test tools! +

+
+ +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`. + +The generated file exports a `withServerpod` helper that enables you to call your endpoints directly like regular functions: + +```dart +// Import the generated file, it contains everything you need. +import 'test_tools/serverpod_test_tools.dart'; + +void main() { + withServerpod('Given Example endpoint', (sessionBuilder, endpoints) { + test('when calling `hello` then should return greeting', () async { + final greeting = + await endpoints.example.hello(sessionBuilder, 'Michael'); + expect(greeting, 'Hello, Michael!'); + }); + }); +} +``` + +A few things to note from the above example: + +- The test tools should be imported from the generated test tools file and not the `serverpod_test` package. +- The `withServerpod` callback takes two parameters: `sessionBuilder` and `endpoints`. + - `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. + - `endpoints` contains all your Serverpod endpoints and lets you call them. + +:::info + +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. + +::: + +Before you can run the test you also need to start the Postgres and Redis: + +```bash +docker-compose up --build --detach +``` + +By default this starts up both the `development` and `test` profiles. To only start one profile, simply add `--profile test` to the command. + +Now the test is ready to be run! diff --git a/docs/06-concepts/18-testing/02-the-basics.md b/docs/06-concepts/18-testing/02-the-basics.md new file mode 100644 index 00000000..789b79c9 --- /dev/null +++ b/docs/06-concepts/18-testing/02-the-basics.md @@ -0,0 +1,207 @@ +# The basics + +## Using `sessionBuilder` to set up a test scenario + +The `withServerpod` helper provides a `session` object that helps with setting up different scenarios for tests. It looks like the following: + +```dart +/// A test specific builder to create a [Session] that for instance can be used to call database methods. +/// The builder can also be passed to endpoint calls. The builder will create a new session for each call. +abstract class TestSessionBuilder { + /// Given the properties set on the session through the `copyWith` method, + /// this returns a serverpod [Session] that has the configured state. + Session build(); + + /// Creates a new unique session with the provided properties. + /// This is useful for setting up different session states in the tests + /// or simulating multiple users. + TestSessionBuilder copyWith({ + AuthenticationOverride? authentication, + bool? enableLogging, + }); +} +``` + +To create a new state, simply call `copyWith` with the new properties and use the new session builder in the endpoint call. + +Below follows examples of some common scenarios. + +### Setting authenticated state + +```dart +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())); + }); + }); +}); +``` + +### Seeding the database + +To seed the database before tests, simply `build` a `session` and pass to the database call exactly the same 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](#rollback-database-configuration) for how to configure this behavior. + +::: + +```dart +withServerpod('Given Products endpoint when authenticated', + (sessionBuilder, endpoints) { + const int userId = 1234; + var authenticatedSession = sessionBuilder + .copyWith( + authentication: AuthenticationOverride.authenticationInfo( + userId, + {Scope('user')}, + ), + ) + .build(); + + setUp(() async { + await Product.db.insert(authenticatedSession, [ + 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(authenticatedSession); + 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: + +```dart +withServerpod( + 'Given Products endpoint when authenticated', + (sessionBuilder, endpoints) { + /* test code */ + }, + runMode: ServerpodRunMode.development, +); +``` + +## Configuration + +The following optional configuration options are available to pass as a second argument to `withServerpod`: + +```dart +{ + RollbackDatabase? rollbackDatabase = RollbackDatabase.afterEach, + String? runMode = ServerpodRunmode.test, + bool? enableSessionLogging = false, + bool? applyMigrations = true, +} +``` + +### `rollbackDatabase` {#rollback-database-configuration} + +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: + +```dart +/// 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 two main 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`. + +```dart +Future 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: + +```dart +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, +); +``` + +### `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`. diff --git a/docs/06-concepts/18-testing/03-advanced-examples.md b/docs/06-concepts/18-testing/03-advanced-examples.md new file mode 100644 index 00000000..0200aac7 --- /dev/null +++ b/docs/06-concepts/18-testing/03-advanced-examples.md @@ -0,0 +1,98 @@ +# Advanced examples + +## Test business logic that depends on `session` + +It is common to break out business logic into modules and keep it separate from the endpoints. If such a module depends on a `Session` object (e.g to interact with the database), then the `withServerpod` helper can still be used and the second `endpoint` argument can simply be ignored: + +```dart +withServerpod('Given decreasing product quantity when quantity is zero', ( + sessionBuilder, + _ /* Ignore */, +) { + var session = sessionBuilder.build(); + + setUp(() async { + await Product.db.insertRow(session, [ + Product( + id: 123, + name: 'Apple', + quantity: 0, + ), + ]); + }); + + test('then should throw `InvalidOperationException`', + () async { + var future = ProductsBusinessLogic.updateQuantity( + session, + id: 123, + decrease: 1, + ); + + await expectLater(future, throwsA(isA())); + }); +}); +``` + +## Multiple users interacting with a shared stream + +For cases where there are multiple users reading from or writing to a stream, such as real-time communication, it can be helpful to validate this behavior in tests. + +Given the following simplified endpoint: + +```dart +class CommunicationExampleEndpoint { + static const sharedStreamName = 'shared-stream'; + Future postNumberToSharedStream(Session session, int number) async { + await session.messages + .postMessage(sharedStreamName, SimpleData(num: number)); + } + + Stream listenForNumbersOnSharedStream(Session session) async* { + var sharedStream = + session.messages.createStream(sharedStreamName); + + await for (var message in sharedStream) { + yield message.num; + } + } +} +``` + +Then a test to verify this behavior can be written as below. Note the call to the `flushEventQueue` helper (exported by the test tools), which ensures that `listenForNumbersOnSharedStream` executes up to its first `yield` statement before continuing with the test. This guarantees that the stream was registered by Serverpod before messages are posted to it. + +```dart +withServerpod('Given CommunicationExampleEndpoint', (sessionBuilder, endpoints) { + const int userId1 = 1; + const int userId2 = 2; + + test( + 'when calling postNumberToSharedStream and listenForNumbersOnSharedStream ' + 'with different sessions then number should be echoed', + () async { + var userSession1 = sessionBuilder.copyWith( + authentication: AuthenticationOverride.authenticationInfo( + userId1, + {}, + ), + ); + var userSession2 = sessionBuilder.copyWith( + authentication: AuthenticationOverride.authenticationInfo( + userId2, + {}, + ), + ); + + var stream = + endpoints.testTools.listenForNumbersOnSharedStream(userSession1); + // Wait for `listenForNumbersOnSharedStream` to execute up to its + // `yield` statement before continuing + await flushEventQueue(); + + await endpoints.testTools.postNumberToSharedStream(userSession2, 111); + await endpoints.testTools.postNumberToSharedStream(userSession2, 222); + + await expectLater(stream.take(2), emitsInOrder([111, 222])); + }); +}); +``` diff --git a/docs/06-concepts/18-testing/04-best-practises.md b/docs/06-concepts/18-testing/04-best-practises.md new file mode 100644 index 00000000..fc63fe2d --- /dev/null +++ b/docs/06-concepts/18-testing/04-best-practises.md @@ -0,0 +1,103 @@ +# Best practises + +## Imports + +### Don't + +```dart +import 'serverpod_test_tools.dart'; +// Don't import `serverpod_test` directly. +import 'package:serverpod_test/serverpod_test.dart'; ❌ +``` + +### Do + +```dart +// Only import the generated test tools file. +// It re-exports all helpers and types that are needed. +import 'serverpod_test_tools.dart'; ✅ +``` + +### Database clean up + +Unless configured otherwise, by default `withServerpod` does all database operations inside a transaction that is rolled back after each `test`. + +### Don't + +```dart +withServerpod('Given ProductsEndpoint', (sessionBuilder, endpoints) { + var session = sessionBuilder.build(); + + setUp(() async { + await Product.db.insertRow(session, Product(name: 'Apple', price: 10)); + }); + + tearDown(() async { + await Product.db.deleteWhere( ❌ // Unnecessary clean up + session, + where: (_) => Constant.bool(true), + ); + }); + + // ... +}); +``` + +### Do + +```dart +withServerpod('Given ProductsEndpoint', (sessionBuilder, endpoints) { + var session = sessionBuilder.build(); + + setUp(() async { + await Product.db.insertRow(session, Product(name: 'Apple', price: 10)); + }); + + ✅ // Clean up can be omitted since the transaction is rolled back after each by default + + // ... +}); +``` + +## Calling endpoints + +While it's technically possible to instantiate an endpoint class and call its methods directly with a Serverpod `Session`, it's advised that you do not. The reason is that lifecycle events and validation that should happen before or after an endpoint method is called is taken care of by the framework. Calling endpoint methods directly would circumvent that and the code would not behave like production code. Using the test tools guarantees that the way endpoints behave during tests is the same as in production. + +### Don't + +```dart +void main() { + // ❌ Don't instantiate endpoints directly + var exampleEndpoint = ExampleEndpoint(); + + withServerpod('Given Example endpoint', ( + sessionBuilder, + _ /* not using the provided endpoints */, + ) { + var session = sessionBuilder.build(); + + test('when calling `hello` then should return greeting', () async { + // ❌ Don't call and endpoint method directly on the endpoint class. + final greeting = await exampleEndpoint.hello(session, 'Michael'); + expect(greeting, 'Hello, Michael!'); + }); + }); +} +``` + +### Do + +```dart +void main() { + withServerpod('Given Example endpoint', (sessionBuilder, endpoints) { + var session = sessionBuilder.build(); + + test('when calling `hello` then should return greeting', () async { + // ✅ Use the provided `endpoints` to call the endpoint that should be tested. + final greeting = + await endpoints.example.hello(session, 'Michael'); + expect(greeting, 'Hello, Michael!'); + }); + }); +} +``` diff --git a/docs/06-concepts/18-testing/_category_.json b/docs/06-concepts/18-testing/_category_.json new file mode 100644 index 00000000..82138d51 --- /dev/null +++ b/docs/06-concepts/18-testing/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Testing", + "collapsed": true +} \ No newline at end of file From a399dcecf554d68109a99b685e3c44f7d1345669 Mon Sep 17 00:00:00 2001 From: Hampus Lavin Date: Tue, 8 Oct 2024 08:26:55 +0200 Subject: [PATCH 02/12] feat: add doc for all exported types --- docs/06-concepts/18-testing/01-get-started.md | 2 +- docs/06-concepts/18-testing/02-the-basics.md | 86 ++++++++++++++++++- .../18-testing/03-advanced-examples.md | 4 +- 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/docs/06-concepts/18-testing/01-get-started.md b/docs/06-concepts/18-testing/01-get-started.md index 7c2dcf1f..8851af7f 100644 --- a/docs/06-concepts/18-testing/01-get-started.md +++ b/docs/06-concepts/18-testing/01-get-started.md @@ -118,7 +118,7 @@ A few things to note from the above example: - The test tools should be imported from the generated test tools file and not the `serverpod_test` package. - The `withServerpod` callback takes two parameters: `sessionBuilder` and `endpoints`. - - `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. + - `sessionBuilder` is used to build a `session` object that represents the state of the world during an endpoint call and is used to set up scenarios. - `endpoints` contains all your Serverpod endpoints and lets you call them. :::info diff --git a/docs/06-concepts/18-testing/02-the-basics.md b/docs/06-concepts/18-testing/02-the-basics.md index 789b79c9..64aaecd4 100644 --- a/docs/06-concepts/18-testing/02-the-basics.md +++ b/docs/06-concepts/18-testing/02-the-basics.md @@ -2,7 +2,7 @@ ## Using `sessionBuilder` to set up a test scenario -The `withServerpod` helper provides a `session` object that helps with setting up different scenarios for tests. It looks like the following: +The `withServerpod` helper provides a `sessionBuilder` object that helps with setting up different scenarios for tests. It looks like the following: ```dart /// A test specific builder to create a [Session] that for instance can be used to call database methods. @@ -24,10 +24,25 @@ abstract class TestSessionBuilder { To create a new state, simply call `copyWith` with the new properties and use the new session builder in the endpoint call. -Below follows examples of some common scenarios. - ### Setting authenticated state +To control the authenticated state of the session, the `AuthenticationOverride` class can be used: + +```dart +/// An override for the authentication state in a test session. +abstract class AuthenticationOverride { + /// Sets the session to be authenticated with the provided userId and scope. + static AuthenticationOverride authenticationInfo( + int userId, Set scopes, + {String? authId}); + + /// Sets the session to be unauthenticated. This is the default. + static AuthenticationOverride unauthenticated(); +} +``` + +Pass it to the `sessionBuilder.copyWith` to simulate different scenarios. Below follows an example for each case: + ```dart withServerpod('Given AuthenticatedExample endpoint', (sessionBuilder, endpoints) { @@ -205,3 +220,68 @@ Wether session logging should be enabled. Defaults to `false`. ### `applyMigrations` Wether pending migrations should be applied when starting Serverpod. Defaults to `true`. + +## Test exceptions + +The following exceptions are exported from the generated test tools file and can be thrown in various scenarios, see below. + +### `ServerpodUnauthenticatedException` + +```dart +/// The user was not authenticated. +class ServerpodUnauthenticatedException implements Exception { + ServerpodUnauthenticatedException(); +} + +``` + +### `ServerpodInsufficientAccessException` + +```dart +/// The authentication key provided did not have sufficient access. +class ServerpodInsufficientAccessException implements Exception { + ServerpodInsufficientAccessException(); +} +``` + +### `InvalidConfigurationException` + +```dart +/// Thrown when an invalid configuration state is found. +class InvalidConfigurationException implements Exception { + final String message; + InvalidConfigurationException(this.message); +} +``` + +### `ConnectionClosedException` + +```dart +/// Thrown if a stream connection is closed with an error. +/// For example, if the user authentication was revoked. +class ConnectionClosedException implements Exception { + const ConnectionClosedException(); +} + +``` + +## Test helpers + +### `flushEventQueue` + +Test helper to flush the event queue. +Useful for waiting for async events to complete before continuing the test. + +```dart +Future 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: + +```dart +var stream = endpoints.someEndoint.generatorFunction(session); +await flushEventQueue(); +``` + +See also [this complete example](advanced-examples#multiple-users-with-stream). diff --git a/docs/06-concepts/18-testing/03-advanced-examples.md b/docs/06-concepts/18-testing/03-advanced-examples.md index 0200aac7..ff532cb5 100644 --- a/docs/06-concepts/18-testing/03-advanced-examples.md +++ b/docs/06-concepts/18-testing/03-advanced-examples.md @@ -1,6 +1,6 @@ # Advanced examples -## Test business logic that depends on `session` +## Test business logic that depends on `Session` It is common to break out business logic into modules and keep it separate from the endpoints. If such a module depends on a `Session` object (e.g to interact with the database), then the `withServerpod` helper can still be used and the second `endpoint` argument can simply be ignored: @@ -34,7 +34,7 @@ withServerpod('Given decreasing product quantity when quantity is zero', ( }); ``` -## Multiple users interacting with a shared stream +## Multiple users interacting with a shared stream {#multiple-users-with-stream} For cases where there are multiple users reading from or writing to a stream, such as real-time communication, it can be helpful to validate this behavior in tests. From 46611486889e21d17608c225b4941df4176f3c08 Mon Sep 17 00:00:00 2001 From: Hampus Lavin Date: Tue, 8 Oct 2024 11:09:09 +0200 Subject: [PATCH 03/12] fix(review): apply suggestions --- docs/06-concepts/18-testing/01-get-started.md | 8 +- docs/06-concepts/18-testing/02-the-basics.md | 85 +++++-------------- .../18-testing/04-best-practises.md | 15 +++- 3 files changed, 38 insertions(+), 70 deletions(-) diff --git a/docs/06-concepts/18-testing/01-get-started.md b/docs/06-concepts/18-testing/01-get-started.md index 8851af7f..7b399f62 100644 --- a/docs/06-concepts/18-testing/01-get-started.md +++ b/docs/06-concepts/18-testing/01-get-started.md @@ -95,7 +95,7 @@ That's it, the project setup should be ready to start using the test tools!

-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`. +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`. The folder name `integration_test` is chosen to differentiate from unit tests (see the [best practises section](best-practises#unit-integration) for more information on this). The generated file exports a `withServerpod` helper that enables you to call your endpoints directly like regular functions: @@ -118,16 +118,16 @@ A few things to note from the above example: - The test tools should be imported from the generated test tools file and not the `serverpod_test` package. - The `withServerpod` callback takes two parameters: `sessionBuilder` and `endpoints`. - - `sessionBuilder` is used to build a `session` object that represents the state of the world during an endpoint call and is used to set up scenarios. + - `sessionBuilder` is used to build a `session` object that represents the server state during an endpoint call and is used to set up scenarios. - `endpoints` contains all your Serverpod endpoints and lets you call them. -:::info +:::tip 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. ::: -Before you can run the test you also need to start the Postgres and Redis: +Before the test can be run the Postgres and Redis also have to be started: ```bash docker-compose up --build --detach diff --git a/docs/06-concepts/18-testing/02-the-basics.md b/docs/06-concepts/18-testing/02-the-basics.md index 64aaecd4..f31c765f 100644 --- a/docs/06-concepts/18-testing/02-the-basics.md +++ b/docs/06-concepts/18-testing/02-the-basics.md @@ -44,8 +44,7 @@ abstract class AuthenticationOverride { Pass it to the `sessionBuilder.copyWith` to simulate different scenarios. Below follows an example for each case: ```dart -withServerpod('Given AuthenticatedExample endpoint', - (sessionBuilder, endpoints) { +withServerpod('Given AuthenticatedExample endpoint', (sessionBuilder, endpoints) { // Corresponds to an actual user id const int userId = 1234; @@ -90,27 +89,18 @@ By default `withServerpod` does all database operations inside a transaction tha ::: ```dart -withServerpod('Given Products endpoint when authenticated', - (sessionBuilder, endpoints) { - const int userId = 1234; - var authenticatedSession = sessionBuilder - .copyWith( - authentication: AuthenticationOverride.authenticationInfo( - userId, - {Scope('user')}, - ), - ) - .build(); +withServerpod('Given Products endpoint', (sessionBuilder, endpoints) { + var session = sessionBuilder.build(); setUp(() async { - await Product.db.insert(authenticatedSession, [ - Product(name: 'Apple', price: 10), - Product(name: 'Banana', price: 10) + 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(authenticatedSession); + final products = await endpoints.products.all(sessionBuilder); expect(products, hasLength(2)); expect(products.map((p) => p.name), contains(['Apple', 'Banana'])); }); @@ -125,7 +115,7 @@ It is possible to override the default run mode by setting the `runMode` setting ```dart withServerpod( - 'Given Products endpoint when authenticated', + 'Given Products endpoint', (sessionBuilder, endpoints) { /* test code */ }, @@ -137,14 +127,12 @@ withServerpod( The following optional configuration options are available to pass as a second argument to `withServerpod`: -```dart -{ - RollbackDatabase? rollbackDatabase = RollbackDatabase.afterEach, - String? runMode = ServerpodRunmode.test, - bool? enableSessionLogging = false, - bool? applyMigrations = true, -} -``` +|Property|Type|Default| +|:-----|:-----|:---:| +|`rollbackDatabase`|`RollbackDatabase?`|`RollbackDatabase.afterEach`| +|`runMode`|`String?`|`ServerpodRunmode.test`| +|`enableSessionLogging`|`bool?`|`false`| +|`applyMigrations`|`bool?`|`true`| ### `rollbackDatabase` {#rollback-database-configuration} @@ -225,45 +213,12 @@ Wether pending migrations should be applied when starting Serverpod. Defaults to The following exceptions are exported from the generated test tools file and can be thrown in various scenarios, see below. -### `ServerpodUnauthenticatedException` - -```dart -/// The user was not authenticated. -class ServerpodUnauthenticatedException implements Exception { - ServerpodUnauthenticatedException(); -} - -``` - -### `ServerpodInsufficientAccessException` - -```dart -/// The authentication key provided did not have sufficient access. -class ServerpodInsufficientAccessException implements Exception { - ServerpodInsufficientAccessException(); -} -``` - -### `InvalidConfigurationException` - -```dart -/// Thrown when an invalid configuration state is found. -class InvalidConfigurationException implements Exception { - final String message; - InvalidConfigurationException(this.message); -} -``` - -### `ConnectionClosedException` - -```dart -/// Thrown if a stream connection is closed with an error. -/// For example, if the user authentication was revoked. -class ConnectionClosedException implements Exception { - const ConnectionClosedException(); -} - -``` +|Exception|Description| +|:-----|:-----| +|`ServerpodUnauthenticatedException`|Thrown during an endpoint method call when the user was not authenticated.| +|`ServerpodInsufficientAccessException`|Thrown during an endpoint method call when the authentication key provided did not have sufficient access.| +|`ConnectionClosedException`|Thrown during an endpoint method call if a stream connection was closed with an error. For example, if the user authentication was revoked.| +|`InvalidConfigurationException`|Thrown when an invalid configuration state is found.| ## Test helpers diff --git a/docs/06-concepts/18-testing/04-best-practises.md b/docs/06-concepts/18-testing/04-best-practises.md index fc63fe2d..01299b38 100644 --- a/docs/06-concepts/18-testing/04-best-practises.md +++ b/docs/06-concepts/18-testing/04-best-practises.md @@ -20,7 +20,7 @@ import 'serverpod_test_tools.dart'; ✅ ### Database clean up -Unless configured otherwise, by default `withServerpod` does all database operations inside a transaction that is rolled back after each `test`. +Unless configured otherwise, by default `withServerpod` does all database operations inside a transaction that is rolled back after each `test` (see [the configuration options](the-basics#rollback-database-configuration) for more info on this behavior). ### Don't @@ -101,3 +101,16 @@ void main() { }); } ``` + +## Unit and integration tests {#unit-integration} + +### Don't + +❌ Mix different types of tests together. It is significantly easier to navigate a project if the different types of tests are grouped. + +### Do + +✅ Have a clear structure for the different types of test. Serverpod recommends the following two folders in the `server`: + +- `test`: Unit tests. +- `integration_test`: Tests for endpoints or business logic modules using the `withServerpod` helper. From cfa7b42b0d6da4d4463ed4ecb9c9368c5ddc0846 Mon Sep 17 00:00:00 2001 From: Hampus Lavin Date: Tue, 8 Oct 2024 16:41:58 +0200 Subject: [PATCH 04/12] fix(review): add intro to each best practice --- docs/06-concepts/18-testing/04-best-practises.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/06-concepts/18-testing/04-best-practises.md b/docs/06-concepts/18-testing/04-best-practises.md index 01299b38..e926b4f2 100644 --- a/docs/06-concepts/18-testing/04-best-practises.md +++ b/docs/06-concepts/18-testing/04-best-practises.md @@ -2,6 +2,8 @@ ## Imports +While it's possible to import types and test helpers from the `serverpod_test`, it's completely redundant. The generated file exports everything that is needed. Adding an additional import is just unnecessary noise and will likely also be flagged as duplicated imports by the Dart linter. + ### Don't ```dart @@ -104,9 +106,11 @@ void main() { ## Unit and integration tests {#unit-integration} +It is significantly easier to navigate a project if the different types of tests are clearly separated. + ### Don't -❌ Mix different types of tests together. It is significantly easier to navigate a project if the different types of tests are grouped. +❌ Mix different types of tests together. ### Do From 642f5f43fd76d601a0615246721ca42b7b81744c Mon Sep 17 00:00:00 2001 From: Hampus Lavin Date: Tue, 8 Oct 2024 16:59:56 +0200 Subject: [PATCH 05/12] fix(review): add better test instructions --- docs/06-concepts/18-testing/01-get-started.md | 8 +++++++- docs/06-concepts/18-testing/02-the-basics.md | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/06-concepts/18-testing/01-get-started.md b/docs/06-concepts/18-testing/01-get-started.md index 7b399f62..eab33231 100644 --- a/docs/06-concepts/18-testing/01-get-started.md +++ b/docs/06-concepts/18-testing/01-get-started.md @@ -135,4 +135,10 @@ docker-compose up --build --detach By default this starts up both the `development` and `test` profiles. To only start one profile, simply add `--profile test` to the command. -Now the test is ready to be run! +Now the test is ready to be run: + +```bash +dart test integration_test +``` + +Happy testing! diff --git a/docs/06-concepts/18-testing/02-the-basics.md b/docs/06-concepts/18-testing/02-the-basics.md index f31c765f..f2149ba3 100644 --- a/docs/06-concepts/18-testing/02-the-basics.md +++ b/docs/06-concepts/18-testing/02-the-basics.md @@ -197,6 +197,14 @@ withServerpod( ); ``` +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: + +```bash +dart test integration_test --concurrency=1 +``` + +For the other cases this is not an issue, as each `withServerpod` has its own transaction and will therefore be isolated. + ### `runMode` The run mode that Serverpod should be running in. Defaults to `test`. From 49270ccf5d84e50bf11645f14d335e1f947ac89b Mon Sep 17 00:00:00 2001 From: Hampus Lavin Date: Tue, 8 Oct 2024 17:02:29 +0200 Subject: [PATCH 06/12] fix(review): clarify exception scope --- docs/06-concepts/18-testing/02-the-basics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/06-concepts/18-testing/02-the-basics.md b/docs/06-concepts/18-testing/02-the-basics.md index f2149ba3..0198e8a9 100644 --- a/docs/06-concepts/18-testing/02-the-basics.md +++ b/docs/06-concepts/18-testing/02-the-basics.md @@ -219,7 +219,7 @@ Wether pending migrations should be applied when starting Serverpod. Defaults to ## Test exceptions -The following exceptions are exported from the generated test tools file and can be thrown in various scenarios, see below. +The following exceptions are exported from the generated test tools file and can be thrown by the test tools in various scenarios, see below. |Exception|Description| |:-----|:-----| From 643687a8e3352c5ea255ec97bf426cee27ee9428 Mon Sep 17 00:00:00 2001 From: Hampus Lavin Date: Tue, 8 Oct 2024 17:29:15 +0200 Subject: [PATCH 07/12] fix(review): add complete file reference --- docs/06-concepts/18-testing/01-get-started.md | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/docs/06-concepts/18-testing/01-get-started.md b/docs/06-concepts/18-testing/01-get-started.md index eab33231..1ac4453b 100644 --- a/docs/06-concepts/18-testing/01-get-started.md +++ b/docs/06-concepts/18-testing/01-get-started.md @@ -37,6 +37,68 @@ redis_test: - test ``` +
+Or copy the complete file here. +

+ +```yaml +services: + # Development services + postgres: + image: postgres:16.3 + ports: + - '8090:5432' + environment: + POSTGRES_USER: postgres + POSTGRES_DB: projectname + POSTGRES_PASSWORD: "" + volumes: + - projectname_data:/var/lib/postgresql/data + profiles: + - '' # Default profile + - dev + redis: + image: redis:6.2.6 + ports: + - '8091:6379' + command: redis-server --requirepass "" + environment: + - REDIS_REPLICATION_MODE=master + profiles: + - '' # Default profile + - dev + + # Test services + postgres_test: + image: postgres:16.3 + ports: + - '9090:5432' + environment: + POSTGRES_USER: postgres_test + POSTGRES_DB: projectname_test + POSTGRES_PASSWORD: "" + volumes: + - projectname_data:/var/lib/postgresql/data + profiles: + - '' # Default profile + - test + redis_test: + image: redis:6.2.6 + ports: + - '9091:6379' + command: redis-server --requirepass "" + environment: + - REDIS_REPLICATION_MODE=master + profiles: + - '' # Default profile + - test + +volumes: + projectname_data: +``` + +

+
3. Create a `test.yaml` file and add it to the `config` directory: ```yaml From 049c08d097a10a2a51b0c28b69e0660a3f307267 Mon Sep 17 00:00:00 2001 From: Hampus Lavin Date: Wed, 9 Oct 2024 10:37:28 +0200 Subject: [PATCH 08/12] fix(review): rework session builder section --- docs/06-concepts/18-testing/02-the-basics.md | 62 +++++++++---------- .../18-testing/03-advanced-examples.md | 2 +- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/docs/06-concepts/18-testing/02-the-basics.md b/docs/06-concepts/18-testing/02-the-basics.md index 0198e8a9..89fe2b7e 100644 --- a/docs/06-concepts/18-testing/02-the-basics.md +++ b/docs/06-concepts/18-testing/02-the-basics.md @@ -2,46 +2,44 @@ ## Using `sessionBuilder` to set up a test scenario -The `withServerpod` helper provides a `sessionBuilder` object that helps with setting up different scenarios for tests. It looks like the following: +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. The copyWith method takes the following named parameters: + +|Property|Type|Default|Description| +|:-----|:-----|:---:|:-----| +|`authentication`|`AuthenticationOverride?`|`AuthenticationOverride.unauthenticated()`|See section [Setting authenticated state](#setting-authenticated-state).| +|`enableLogging`|`bool?`|`false`|Wether 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](#setting-authenticated-state) for an example). + +To build out a `Session` (to use for [database calls](#seeding-the-database) or [pass on to functions](advanced-examples##business-logic-depends-on-session)), simply call the `build` method: ```dart -/// A test specific builder to create a [Session] that for instance can be used to call database methods. -/// The builder can also be passed to endpoint calls. The builder will create a new session for each call. -abstract class TestSessionBuilder { - /// Given the properties set on the session through the `copyWith` method, - /// this returns a serverpod [Session] that has the configured state. - Session build(); - - /// Creates a new unique session with the provided properties. - /// This is useful for setting up different session states in the tests - /// or simulating multiple users. - TestSessionBuilder copyWith({ - AuthenticationOverride? authentication, - bool? enableLogging, - }); -} +Session session = sessionBuilder.build(); ``` -To create a new state, simply call `copyWith` with the new properties and use the new session builder in the endpoint call. +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 {#setting-authenticated-state} -### Setting authenticated state +To control the authenticated state of the session, the `AuthenticationOverride` class can be used. -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()`: ```dart -/// An override for the authentication state in a test session. -abstract class AuthenticationOverride { - /// Sets the session to be authenticated with the provided userId and scope. - static AuthenticationOverride authenticationInfo( - int userId, Set scopes, - {String? authId}); - - /// Sets the session to be unauthenticated. This is the default. - static AuthenticationOverride unauthenticated(); -} +static AuthenticationOverride unauthenticated(); +``` + +To create an authenticated override, call `AuthenticationOverride.authenticationInfo(...)`: + +```dart +static AuthenticationOverride authenticationInfo( + int userId, + Set scopes, { + String? authId, +}) ``` -Pass it to the `sessionBuilder.copyWith` to simulate different scenarios. Below follows an example for each case: +Pass these to `sessionBuilder.copyWith` to simulate different scenarios. Below follows an example for each case: ```dart withServerpod('Given AuthenticatedExample endpoint', (sessionBuilder, endpoints) { @@ -78,9 +76,9 @@ withServerpod('Given AuthenticatedExample endpoint', (sessionBuilder, endpoints) }); ``` -### Seeding the database +### Seeding the database {#seeding-the-database} -To seed the database before tests, simply `build` a `session` and pass to the database call exactly the same as in production code. +To seed the database before tests, `build` a `session` and pass it to the database call just as in production code. :::info diff --git a/docs/06-concepts/18-testing/03-advanced-examples.md b/docs/06-concepts/18-testing/03-advanced-examples.md index ff532cb5..8efcca20 100644 --- a/docs/06-concepts/18-testing/03-advanced-examples.md +++ b/docs/06-concepts/18-testing/03-advanced-examples.md @@ -1,6 +1,6 @@ # Advanced examples -## Test business logic that depends on `Session` +## Test business logic that depends on `Session` {#business-logic-depends-on-session} It is common to break out business logic into modules and keep it separate from the endpoints. If such a module depends on a `Session` object (e.g to interact with the database), then the `withServerpod` helper can still be used and the second `endpoint` argument can simply be ignored: From 3ce1878b1b8413fa595552b53dad01c46b1c4cb4 Mon Sep 17 00:00:00 2001 From: Hampus Lavin Date: Wed, 9 Oct 2024 10:40:05 +0200 Subject: [PATCH 09/12] fix: minor rephrase --- docs/06-concepts/18-testing/02-the-basics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/06-concepts/18-testing/02-the-basics.md b/docs/06-concepts/18-testing/02-the-basics.md index 89fe2b7e..30c0fbcb 100644 --- a/docs/06-concepts/18-testing/02-the-basics.md +++ b/docs/06-concepts/18-testing/02-the-basics.md @@ -2,7 +2,7 @@ ## Using `sessionBuilder` to 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. The copyWith method takes the following named parameters: +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: |Property|Type|Default|Description| |:-----|:-----|:---:|:-----| From 77e69c68795768e910cda49e040d87f69fc3984c Mon Sep 17 00:00:00 2001 From: Hampus Lavin Date: Wed, 9 Oct 2024 11:38:17 +0200 Subject: [PATCH 10/12] fix: add experimental-flag, add serverpod mini notice --- docs/06-concepts/18-testing/01-get-started.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/06-concepts/18-testing/01-get-started.md b/docs/06-concepts/18-testing/01-get-started.md index 1ac4453b..c88e7b37 100644 --- a/docs/06-concepts/18-testing/01-get-started.md +++ b/docs/06-concepts/18-testing/01-get-started.md @@ -2,10 +2,16 @@ Serverpod provides simple but feature rich test tools to make testing your backend a breeze. +:::info + +For Serverpod Mini projects, everything related to the database in this guide can be ignored. + +::: +
Have an existing project? Follow these steps first!

-For existing projects, a few extra things need to be done: +For existing non-Mini projects, a few extra things need to be done: 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). 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: @@ -157,7 +163,7 @@ That's it, the project setup should be ready to start using the test tools!

-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`. The folder name `integration_test` is chosen to differentiate from unit tests (see the [best practises section](best-practises#unit-integration) for more information on this). +Go to the server directory and generate the test tools by running `serverpod generate --experimental-features testTools`. The default location for the generated file is `integration_test/test_tools/serverpod_test_tools.dart`. The folder name `integration_test` is chosen to differentiate from unit tests (see the [best practises section](best-practises#unit-integration) for more information on this). The generated file exports a `withServerpod` helper that enables you to call your endpoints directly like regular functions: From 1a07bbf0b9f6a00855c0f4615aef2c742bc6666c Mon Sep 17 00:00:00 2001 From: Hampus Lavin Date: Thu, 10 Oct 2024 09:01:29 +0200 Subject: [PATCH 11/12] fix(review): rephrase, remove redundant header ids --- docs/06-concepts/18-testing/01-get-started.md | 2 +- docs/06-concepts/18-testing/02-the-basics.md | 10 +++++----- docs/06-concepts/18-testing/03-advanced-examples.md | 4 ++-- docs/06-concepts/18-testing/04-best-practises.md | 7 ++++++- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/06-concepts/18-testing/01-get-started.md b/docs/06-concepts/18-testing/01-get-started.md index c88e7b37..eebf3293 100644 --- a/docs/06-concepts/18-testing/01-get-started.md +++ b/docs/06-concepts/18-testing/01-get-started.md @@ -163,7 +163,7 @@ That's it, the project setup should be ready to start using the test tools!

-Go to the server directory and generate the test tools by running `serverpod generate --experimental-features testTools`. The default location for the generated file is `integration_test/test_tools/serverpod_test_tools.dart`. The folder name `integration_test` is chosen to differentiate from unit tests (see the [best practises section](best-practises#unit-integration) for more information on this). +Go to the server directory and generate the test tools by running `serverpod generate --experimental-features testTools`. The default location for the generated file is `integration_test/test_tools/serverpod_test_tools.dart`. The folder name `integration_test` is chosen to differentiate from unit tests (see the [best practises section](best-practises#unit-and-integration-tests) for more information on this). The generated file exports a `withServerpod` helper that enables you to call your endpoints directly like regular functions: diff --git a/docs/06-concepts/18-testing/02-the-basics.md b/docs/06-concepts/18-testing/02-the-basics.md index 30c0fbcb..ddd24ba3 100644 --- a/docs/06-concepts/18-testing/02-the-basics.md +++ b/docs/06-concepts/18-testing/02-the-basics.md @@ -1,6 +1,6 @@ # The basics -## Using `sessionBuilder` to set up a test scenario +## 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: @@ -11,7 +11,7 @@ The `withServerpod` helper provides a `sessionBuilder` that helps with setting u 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](#setting-authenticated-state) for an example). -To build out a `Session` (to use for [database calls](#seeding-the-database) or [pass on to functions](advanced-examples##business-logic-depends-on-session)), simply call the `build` method: +To build out a `Session` (to use for [database calls](#seeding-the-database) or [pass on to functions](advanced-examples#test-business-logic-that-depends-on-session)), simply call the `build` method: ```dart Session session = sessionBuilder.build(); @@ -19,7 +19,7 @@ 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 {#setting-authenticated-state} +### Setting authenticated state To control the authenticated state of the session, the `AuthenticationOverride` class can be used. @@ -76,7 +76,7 @@ withServerpod('Given AuthenticatedExample endpoint', (sessionBuilder, endpoints) }); ``` -### Seeding the database {#seeding-the-database} +### Seeding the database To seed the database before tests, `build` a `session` and pass it to the database call just as in production code. @@ -245,4 +245,4 @@ var stream = endpoints.someEndoint.generatorFunction(session); await flushEventQueue(); ``` -See also [this complete example](advanced-examples#multiple-users-with-stream). +See also [this complete example](advanced-examples#multiple-users-interacting-with-a-shared-stream). diff --git a/docs/06-concepts/18-testing/03-advanced-examples.md b/docs/06-concepts/18-testing/03-advanced-examples.md index 8efcca20..6fa1057c 100644 --- a/docs/06-concepts/18-testing/03-advanced-examples.md +++ b/docs/06-concepts/18-testing/03-advanced-examples.md @@ -1,6 +1,6 @@ # Advanced examples -## Test business logic that depends on `Session` {#business-logic-depends-on-session} +## Test business logic that depends on `Session` It is common to break out business logic into modules and keep it separate from the endpoints. If such a module depends on a `Session` object (e.g to interact with the database), then the `withServerpod` helper can still be used and the second `endpoint` argument can simply be ignored: @@ -34,7 +34,7 @@ withServerpod('Given decreasing product quantity when quantity is zero', ( }); ``` -## Multiple users interacting with a shared stream {#multiple-users-with-stream} +## Multiple users interacting with a shared stream For cases where there are multiple users reading from or writing to a stream, such as real-time communication, it can be helpful to validate this behavior in tests. diff --git a/docs/06-concepts/18-testing/04-best-practises.md b/docs/06-concepts/18-testing/04-best-practises.md index e926b4f2..045ec373 100644 --- a/docs/06-concepts/18-testing/04-best-practises.md +++ b/docs/06-concepts/18-testing/04-best-practises.md @@ -1,3 +1,8 @@ +--- +# Don't display do's and don'ts in the table of contents +toc_max_heading_level: 2 +--- + # Best practises ## Imports @@ -104,7 +109,7 @@ void main() { } ``` -## Unit and integration tests {#unit-integration} +## Unit and integration tests It is significantly easier to navigate a project if the different types of tests are clearly separated. From abc0bbdaa2819e9b0c9a6fb98e24bdd27a61174c Mon Sep 17 00:00:00 2001 From: Hampus Lavin Date: Thu, 10 Oct 2024 09:33:49 +0200 Subject: [PATCH 12/12] fix(review): remove redundant comment --- docs/06-concepts/18-testing/03-advanced-examples.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/06-concepts/18-testing/03-advanced-examples.md b/docs/06-concepts/18-testing/03-advanced-examples.md index 6fa1057c..8266ea88 100644 --- a/docs/06-concepts/18-testing/03-advanced-examples.md +++ b/docs/06-concepts/18-testing/03-advanced-examples.md @@ -7,7 +7,7 @@ It is common to break out business logic into modules and keep it separate from ```dart withServerpod('Given decreasing product quantity when quantity is zero', ( sessionBuilder, - _ /* Ignore */, + _, ) { var session = sessionBuilder.build();