From 8c2b1b7b73a5dc5271c4139c6641d3f685486c69 Mon Sep 17 00:00:00 2001 From: Isakdl Date: Thu, 2 Jan 2025 17:20:18 +0100 Subject: [PATCH 1/9] refactor: Move contribute guidelines file --- docs/{10-contribute.md => 10-contribute/01-contribute.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename docs/{10-contribute.md => 10-contribute/01-contribute.md} (99%) diff --git a/docs/10-contribute.md b/docs/10-contribute/01-contribute.md similarity index 99% rename from docs/10-contribute.md rename to docs/10-contribute/01-contribute.md index f104d911..fa5e4494 100644 --- a/docs/10-contribute.md +++ b/docs/10-contribute/01-contribute.md @@ -5,6 +5,7 @@ Serverpod is built by the community for the community. Pull requests are very mu
## Roadmap + If you want to contribute, please view our [roadmap](https://github.com/orgs/serverpod/projects/4) to make sure your contribution is in-line with our plans for future development. This will make it much more likely that we can include the new features you are building. You can also check our list of [good first issues](https://github.com/serverpod/serverpod/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). :::important @@ -109,7 +110,6 @@ If you are contributing new code, you will also need to provide tests for your c Feel free to post on [Serverpod's discussion board](https://github.com/serverpod/serverpod/discussions) if you have any questions. We check the board daily. - ## Repository overview Serverpod is a large project and contains many parts. Here is a quick overview of how Serverpod is structured and where to find relevant files. @@ -142,4 +142,4 @@ These are 1st party modules for Serverpod. Currently, we maintain an authenticat ### `integrations` -These are integrations for 3rd party services, such as Cloud storage. \ No newline at end of file +These are integrations for 3rd party services, such as Cloud storage. From 0a9d1a27642bc204cdff410cfb06cda4f325fd01 Mon Sep 17 00:00:00 2001 From: Isakdl Date: Thu, 2 Jan 2025 17:20:38 +0100 Subject: [PATCH 2/9] feat: Add testing contribution guideline --- docs/10-contribute/02-testing-guideline.md | 258 +++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 docs/10-contribute/02-testing-guideline.md diff --git a/docs/10-contribute/02-testing-guideline.md b/docs/10-contribute/02-testing-guideline.md new file mode 100644 index 00000000..54bcc2ea --- /dev/null +++ b/docs/10-contribute/02-testing-guideline.md @@ -0,0 +1,258 @@ +# Serverpod Testing Philosophy + +## Overview + +At Serverpod, our core testing philosophy revolves around achieving the following goals: + +- **Readable and Self-Explanatory Tests** – Tests should be easy to understand at a glance. Descriptions must clearly convey the purpose and expected outcome without needing to inspect the test's internal implementation. +- **Resilient to Refactoring** – Tests should not break due to internal refactoring. As long as the external behavior remains consistent, tests should pass regardless of code structure changes. +- **Focused on Behavior, Not Implementation** – We prioritize testing how the code behaves rather than how it is implemented. This prevents unnecessary coupling between tests and production code, fostering long-term stability. +- **Easy to Maintain and Expand** – Tests should be simple to update or extend as the product evolves. Adding new features should not require widespread changes to existing tests. +- **Effective at Catching Bugs** – The primary goal of testing is to identify and prevent bugs. Our tests are crafted to cover edge cases, ensure proper functionality, and catch potential regressions. + +By adhering to the following principles, we ensure that our test suite remains a valuable and reliable asset as our codebase grows. + +This document outlines Serverpod's approach to testing code. It serves as a guide for writing effective, maintainable, and meaningful tests across all our projects. + +## Key Principles + +### 0. Test Independence + +- **Tests should be completely independent of one another.** +- The outcome of a test must never depend on any other test running before or after it. +- The order in which tests are executed **should not matter.** +- Running a single test in isolation must produce the same result as running it alongside others. +- **Exception to the rule:** e2e and integration tests. In scenarios where an external state (like a shared database) is involved, tests may require concurrency mode 1 to prevent interference. But each test should start and end in a clean state. + +### 1. Clear and Descriptive Test Descriptions + +- **Test descriptions should be understandable without reading the test code.** +- If a test fails, the description alone should make it clear what went wrong. +- **Format:** Descriptions follow the "Given, When, Then" style. + +**Example:** + +```dart +// Given a user with insufficient permissions +// When attempting to access a restricted page +// Then a 403 Forbidden error is returned +``` + +### 2. Focus on Single Responsibility + +- Each test should **only test one thing**. +- **Avoid** bundling multiple independent checks into a single test. +- It is acceptable to **repeat the same precondition and action** across tests to ensure each aspect is tested individually. + +**Example:** + +```dart +// Good - Tests are split +test('Given an empty list when a string is added then it appears at the first index', () { + final list = []; + list.add('hello'); + expect(list[0], 'hello'); +}); + +test('Given an empty list when a string is added then list length increases by one', () { + final list = []; + list.add('hello'); + expect(list.length, 1); +}); + +// Bad - Multiple independent checks in one test +test('Add string to list and check index and length', () { + final list = []; + list.add('hello'); + expect(list[0], 'hello'); + expect(list.length, 1); +}); +``` + +- Multiple `expect` statements are not necessarily against this rule. However, they must check values that are interdependent and only meaningful when evaluated together. + +**Example:** + +```dart + test('Given a missing semicolon when validated then the entire row is highlighted', () { + final code = 'final a = 1'; + final result = validateCode(code); + + expect(result.span.start.column, 1); + expect(result.span.end.column, 12); + expect(result.span.start.line, 1); + expect(result.span.end.line, 1); + }); +``` + +- In this case, verifying both the start and end positions of the `SourceSpanException` is essential because they collectively describe the error location, and their correctness is interdependent. + +\*Note: SourceSpanException is an object that describes a code error in a source file. See: \*[*https://api.flutter.dev/flutter/package-source\_span\_source\_span/SourceSpanException-class.html*](https://api.flutter.dev/flutter/package-source_span_source_span/SourceSpanException-class.html) + +### 3. Pure Unit Testing + +- **Unit tests should avoid mocking and side effects.** +- Production code should push side effects **up the call stack**, allowing tests to cover pure methods. +- **Test the logical feature/unit** rather than a single method or class. + +**Example:** + +```dart +// Avoid mocking HTTP requests directly, test the logic that processes the response +Future fetchUserData() async { + final response = await httpClient.get(url); + return processResponse(response); +} +``` + +- Test `processResponse` directly without mocking the `httpClient`. + +### 4. Implementation-Agnostic Tests + +- **Do not couple tests to implementation details.** +- Tests should only break if the behavior changes, not when refactoring code. +- **Unit tests must avoid knowledge of internal methods or variables.** +- The use of `@visibleForTesting` is discouraged as it exposes internal details that should remain hidden. This annotation can lead to brittle tests that break during refactoring, even if external behavior remains the same. + +**Example:** + +```dart +// Good - Tests against behavior +String processUserData(String rawData) { + return rawData.toUpperCase(); +} + +test('Given a user name when processed then the name is capitalized', () { + final result = processUserData('john'); + expect(result, 'JOHN'); +}); + +// Bad - Tests against internal methods or state +class UserDataProcessor { + String process(String rawData) { + return _toUpperCase(rawData); + } + + @visibleForTesting + String toUpperCase(String input) => input.toUpperCase(); +} + +// Bad - Verifying internal method call +test('Given a user name when processed then toUpperCase is called', () { + final processor = UserDataProcessor(); + + when(() => processor.toUpperCase('john')).thenReturn('JOHN'); + + final result = processor.process('john'); + + expect(result, 'JOHN'); + verify(() => processor.toUpperCase('john')).called(1); +}); +``` + +- In the bad example, the test verifies that an internal method (`_toUpperCase`) is called, coupling the test to the implementation. The good example validates only the output, ensuring the test focuses on behavior rather than internal details. + +### 5. Immutable Test Philosophy + +- **Tests should rarely be modified.** +- When functionality changes, **add new tests** and/or **remove obsolete ones**. +- Avoid altering existing tests unless fixing bugs, e.g. invalid tests. + +### 6. Simplicity Over Abstraction + +- The explicit examples make it clear what is being tested and reduce the need to jump between methods. The abstracted logic example hides test behavior, making it harder to understand specific test scenarios at a glance. +- **Avoid abstraction in tests.** +- Tests should be **simple, explicit, and easy to read.** +- Logic or shared test functionality should be **copied** rather than abstracted. + +**Examples:** + +```dart + // Good - Explicit test + test('Given a number below threshold when validated then an error is collected', () { + final result = validateNumber(3); + + expect(result.errors.first.message, 'Number must be 5 or greater.'); + }); + + // Good - Explicit test for out of range value + test('Given a number above range when validated then range error is collected', () { + final result = validateNumber(11); + + expect(result.errors.first.message, 'Number must not exceed 10.'); + }); + + // Bad - Abstracted logic into a method + void performValidationTest(int number, String expectedErrorMessage) { + final result = validateNumber(number); + + expect(result.errors.first.message, expectedErrorMessage); + } + + test('Number below threshold', () { + performValidationTest(3, 'Number must be 5 or greater.'); + }); + + test('Number above range', () { + performValidationTest(11, 'Number must not exceed 10.'); + }); +``` + +### 7. Beneficial Abstractions - Test Builders + +- One abstraction we encourage is the **test builder pattern** for constructing test input objects. +- Builders help create logical, valid objects while keeping tests simple and readable. + +**Key Characteristics:** + +- Builder class names follow the pattern: `ObjectNameBuilder`. +- Methods that set properties start with `with`, and return the builder instance (`this`). +- Builders always include a `build` method that returns the final constructed object. +- Constructors provide reasonable defaults for all properties to ensure valid object creation by default. + +**Guidelines:** + +- In tests, always set the properties that are important for the specific scenario. +- Defaults allow seamless addition of new properties without modifying existing tests. +- Using builders ensures objects are created in a realistic, lifecycle-accurate way. + +**Example:** + +```dart +class UserBuilder { + String _name = 'John Doe'; + int _age = 25; + + UserBuilder withName(String name) { + _name = name; + return this; + } + + UserBuilder withAge(int age) { + _age = age; + return this; + } + + User build() { + return User(name: _name, age: _age); + } +} + +// Test Example + test('Given a user at the legal cut of when checking if they are allowed to drink then they are', () { + final user = UserBuilder().withAge(18).build(); + final isAllowed = isLegalDrinkingAge(user); + expect(isAllowed, isTrue); + }); +``` + +- **Benefits:** + - Reduces test brittleness when refactoring. + - Ensures tests continue to produce valid objects as the code evolves. + - Simplifies object creation without requiring deep lifecycle knowledge. + +## Final Thoughts + +Consistent application of these principles leads to a robust and maintainable codebase, fostering confidence in our product's reliability and scalability. + +We acknowledge that there are exceptions to the rules. But when deviating from the guidelines mentioned above you have to argue for why. You must have a reasonable argument that aligns with the core goals. From b385a3d0107413060af3854007dc97d54e91e609 Mon Sep 17 00:00:00 2001 From: Isakdl Date: Fri, 24 Jan 2025 13:53:23 +0100 Subject: [PATCH 3/9] docs: Remove white space --- docs/10-contribute/02-testing-guideline.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/10-contribute/02-testing-guideline.md b/docs/10-contribute/02-testing-guideline.md index 54bcc2ea..20c91b93 100644 --- a/docs/10-contribute/02-testing-guideline.md +++ b/docs/10-contribute/02-testing-guideline.md @@ -255,4 +255,4 @@ class UserBuilder { Consistent application of these principles leads to a robust and maintainable codebase, fostering confidence in our product's reliability and scalability. -We acknowledge that there are exceptions to the rules. But when deviating from the guidelines mentioned above you have to argue for why. You must have a reasonable argument that aligns with the core goals. +We acknowledge that there are exceptions to the rules. But when deviating from the guidelines mentioned above you have to argue for why. You must have a reasonable argument that aligns with the core goals. From 9ecc30fb612fb88bf7b8f02ad06b4cd4f582c4b9 Mon Sep 17 00:00:00 2001 From: Isakdl Date: Fri, 24 Jan 2025 13:54:37 +0100 Subject: [PATCH 4/9] docs: Add additional clerification --- docs/10-contribute/02-testing-guideline.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/10-contribute/02-testing-guideline.md b/docs/10-contribute/02-testing-guideline.md index 20c91b93..81782218 100644 --- a/docs/10-contribute/02-testing-guideline.md +++ b/docs/10-contribute/02-testing-guideline.md @@ -110,7 +110,7 @@ Future fetchUserData() async { ### 4. Implementation-Agnostic Tests - **Do not couple tests to implementation details.** -- Tests should only break if the behavior changes, not when refactoring code. +- Tests should only break if the behavior changes, not when refactoring code. This may be referred to as black box testing. - **Unit tests must avoid knowledge of internal methods or variables.** - The use of `@visibleForTesting` is discouraged as it exposes internal details that should remain hidden. This annotation can lead to brittle tests that break during refactoring, even if external behavior remains the same. From 64923a922232c83d46b9520ca74bb3d8ae591445 Mon Sep 17 00:00:00 2001 From: Isakdl Date: Fri, 24 Jan 2025 14:01:37 +0100 Subject: [PATCH 5/9] docs: Convert to do and don't --- docs/10-contribute/02-testing-guideline.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/10-contribute/02-testing-guideline.md b/docs/10-contribute/02-testing-guideline.md index 81782218..3d6f67e8 100644 --- a/docs/10-contribute/02-testing-guideline.md +++ b/docs/10-contribute/02-testing-guideline.md @@ -47,7 +47,7 @@ This document outlines Serverpod's approach to testing code. It serves as a gui **Example:** ```dart -// Good - Tests are split +// Do - Tests are split test('Given an empty list when a string is added then it appears at the first index', () { final list = []; list.add('hello'); @@ -60,7 +60,7 @@ test('Given an empty list when a string is added then list length increases by o expect(list.length, 1); }); -// Bad - Multiple independent checks in one test +// Don't - Multiple independent checks in one test test('Add string to list and check index and length', () { final list = []; list.add('hello'); @@ -98,7 +98,7 @@ test('Add string to list and check index and length', () { **Example:** ```dart -// Avoid mocking HTTP requests directly, test the logic that processes the response +// Don't mock HTTP requests directly, test the logic that processes the response Future fetchUserData() async { final response = await httpClient.get(url); return processResponse(response); @@ -117,7 +117,7 @@ Future fetchUserData() async { **Example:** ```dart -// Good - Tests against behavior +// Do - Tests against behavior String processUserData(String rawData) { return rawData.toUpperCase(); } @@ -127,7 +127,7 @@ test('Given a user name when processed then the name is capitalized', () { expect(result, 'JOHN'); }); -// Bad - Tests against internal methods or state +// Don't - Tests against internal methods or state class UserDataProcessor { String process(String rawData) { return _toUpperCase(rawData); @@ -137,7 +137,7 @@ class UserDataProcessor { String toUpperCase(String input) => input.toUpperCase(); } -// Bad - Verifying internal method call +// Don't - Verifying internal method call test('Given a user name when processed then toUpperCase is called', () { final processor = UserDataProcessor(); @@ -168,21 +168,21 @@ test('Given a user name when processed then toUpperCase is called', () { **Examples:** ```dart - // Good - Explicit test + // Do - Explicit test test('Given a number below threshold when validated then an error is collected', () { final result = validateNumber(3); expect(result.errors.first.message, 'Number must be 5 or greater.'); }); - // Good - Explicit test for out of range value + // Do - Explicit test for out of range value test('Given a number above range when validated then range error is collected', () { final result = validateNumber(11); expect(result.errors.first.message, 'Number must not exceed 10.'); }); - // Bad - Abstracted logic into a method + // Don't - Abstracted logic into a method void performValidationTest(int number, String expectedErrorMessage) { final result = validateNumber(number); From 8eb46f4fd228500152ad0b682d161554902d981d Mon Sep 17 00:00:00 2001 From: Isakdl Date: Fri, 24 Jan 2025 14:02:03 +0100 Subject: [PATCH 6/9] docs: Reword sentence --- docs/10-contribute/02-testing-guideline.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/10-contribute/02-testing-guideline.md b/docs/10-contribute/02-testing-guideline.md index 3d6f67e8..8b9a83a8 100644 --- a/docs/10-contribute/02-testing-guideline.md +++ b/docs/10-contribute/02-testing-guideline.md @@ -255,4 +255,4 @@ class UserBuilder { Consistent application of these principles leads to a robust and maintainable codebase, fostering confidence in our product's reliability and scalability. -We acknowledge that there are exceptions to the rules. But when deviating from the guidelines mentioned above you have to argue for why. You must have a reasonable argument that aligns with the core goals. +We acknowledge that there are exceptions to the rules. However, any deviation from the guidelines outlined above must be supported by a well-reasoned argument that aligns with the core objectives. From b540f8659e8710bf2867b0382adddbb210564113 Mon Sep 17 00:00:00 2001 From: Isakdl Date: Fri, 24 Jan 2025 14:08:51 +0100 Subject: [PATCH 7/9] docs: Clarify test descriptions --- docs/10-contribute/02-testing-guideline.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/10-contribute/02-testing-guideline.md b/docs/10-contribute/02-testing-guideline.md index 8b9a83a8..e0e34e40 100644 --- a/docs/10-contribute/02-testing-guideline.md +++ b/docs/10-contribute/02-testing-guideline.md @@ -27,8 +27,9 @@ This document outlines Serverpod's approach to testing code. It serves as a gui ### 1. Clear and Descriptive Test Descriptions - **Test descriptions should be understandable without reading the test code.** -- If a test fails, the description alone should make it clear what went wrong. +- If a test fails, the description alone should make it clear what behavior went wrong - **Format:** Descriptions follow the "Given, When, Then" style. + Given [a state or precondition] when [doing an action] then [a result is achieved] **Example:** From 076d20480354417a2127dd501e40b3f5c223a886 Mon Sep 17 00:00:00 2001 From: Isakdl Date: Fri, 24 Jan 2025 14:09:47 +0100 Subject: [PATCH 8/9] docs: Renumber headlines --- docs/10-contribute/02-testing-guideline.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/10-contribute/02-testing-guideline.md b/docs/10-contribute/02-testing-guideline.md index e0e34e40..602ba496 100644 --- a/docs/10-contribute/02-testing-guideline.md +++ b/docs/10-contribute/02-testing-guideline.md @@ -16,7 +16,7 @@ This document outlines Serverpod's approach to testing code. It serves as a gui ## Key Principles -### 0. Test Independence +### 1. Test Independence - **Tests should be completely independent of one another.** - The outcome of a test must never depend on any other test running before or after it. @@ -24,7 +24,7 @@ This document outlines Serverpod's approach to testing code. It serves as a gui - Running a single test in isolation must produce the same result as running it alongside others. - **Exception to the rule:** e2e and integration tests. In scenarios where an external state (like a shared database) is involved, tests may require concurrency mode 1 to prevent interference. But each test should start and end in a clean state. -### 1. Clear and Descriptive Test Descriptions +### 2. Clear and Descriptive Test Descriptions - **Test descriptions should be understandable without reading the test code.** - If a test fails, the description alone should make it clear what behavior went wrong @@ -39,7 +39,7 @@ This document outlines Serverpod's approach to testing code. It serves as a gui // Then a 403 Forbidden error is returned ``` -### 2. Focus on Single Responsibility +### 3. Focus on Single Responsibility - Each test should **only test one thing**. - **Avoid** bundling multiple independent checks into a single test. @@ -90,7 +90,7 @@ test('Add string to list and check index and length', () { \*Note: SourceSpanException is an object that describes a code error in a source file. See: \*[*https://api.flutter.dev/flutter/package-source\_span\_source\_span/SourceSpanException-class.html*](https://api.flutter.dev/flutter/package-source_span_source_span/SourceSpanException-class.html) -### 3. Pure Unit Testing +### 4. Pure Unit Testing - **Unit tests should avoid mocking and side effects.** - Production code should push side effects **up the call stack**, allowing tests to cover pure methods. @@ -108,7 +108,7 @@ Future fetchUserData() async { - Test `processResponse` directly without mocking the `httpClient`. -### 4. Implementation-Agnostic Tests +### 5. Implementation-Agnostic Tests - **Do not couple tests to implementation details.** - Tests should only break if the behavior changes, not when refactoring code. This may be referred to as black box testing. @@ -153,13 +153,13 @@ test('Given a user name when processed then toUpperCase is called', () { - In the bad example, the test verifies that an internal method (`_toUpperCase`) is called, coupling the test to the implementation. The good example validates only the output, ensuring the test focuses on behavior rather than internal details. -### 5. Immutable Test Philosophy +### 6. Immutable Test Philosophy - **Tests should rarely be modified.** - When functionality changes, **add new tests** and/or **remove obsolete ones**. - Avoid altering existing tests unless fixing bugs, e.g. invalid tests. -### 6. Simplicity Over Abstraction +### 7. Simplicity Over Abstraction - The explicit examples make it clear what is being tested and reduce the need to jump between methods. The abstracted logic example hides test behavior, making it harder to understand specific test scenarios at a glance. - **Avoid abstraction in tests.** From f7a35f0c2a7b7216a5f240f8d2ca47a2ee948fb5 Mon Sep 17 00:00:00 2001 From: Isakdl Date: Fri, 24 Jan 2025 14:44:18 +0100 Subject: [PATCH 9/9] docs: Lowercase in titles --- docs/10-contribute/02-testing-guideline.md | 34 +++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/10-contribute/02-testing-guideline.md b/docs/10-contribute/02-testing-guideline.md index 602ba496..771606b4 100644 --- a/docs/10-contribute/02-testing-guideline.md +++ b/docs/10-contribute/02-testing-guideline.md @@ -1,22 +1,22 @@ -# Serverpod Testing Philosophy +# Serverpod testing philosophy ## Overview At Serverpod, our core testing philosophy revolves around achieving the following goals: -- **Readable and Self-Explanatory Tests** – Tests should be easy to understand at a glance. Descriptions must clearly convey the purpose and expected outcome without needing to inspect the test's internal implementation. -- **Resilient to Refactoring** – Tests should not break due to internal refactoring. As long as the external behavior remains consistent, tests should pass regardless of code structure changes. -- **Focused on Behavior, Not Implementation** – We prioritize testing how the code behaves rather than how it is implemented. This prevents unnecessary coupling between tests and production code, fostering long-term stability. -- **Easy to Maintain and Expand** – Tests should be simple to update or extend as the product evolves. Adding new features should not require widespread changes to existing tests. -- **Effective at Catching Bugs** – The primary goal of testing is to identify and prevent bugs. Our tests are crafted to cover edge cases, ensure proper functionality, and catch potential regressions. +- **Readable and self-explanatory tests** – Tests should be easy to understand at a glance. Descriptions must clearly convey the purpose and expected outcome without needing to inspect the test's internal implementation. +- **Resilient to refactoring** – Tests should not break due to internal refactoring. As long as the external behavior remains consistent, tests should pass regardless of code structure changes. +- **Focused on behavior, not implementation** – We prioritize testing how the code behaves rather than how it is implemented. This prevents unnecessary coupling between tests and production code, fostering long-term stability. +- **Easy to maintain and expand** – Tests should be simple to update or extend as the product evolves. Adding new features should not require widespread changes to existing tests. +- **Effective at catching bugs** – The primary goal of testing is to identify and prevent bugs. Our tests are crafted to cover edge cases, ensure proper functionality, and catch potential regressions. By adhering to the following principles, we ensure that our test suite remains a valuable and reliable asset as our codebase grows. This document outlines Serverpod's approach to testing code. It serves as a guide for writing effective, maintainable, and meaningful tests across all our projects. -## Key Principles +## Key principles -### 1. Test Independence +### 1. Test independence - **Tests should be completely independent of one another.** - The outcome of a test must never depend on any other test running before or after it. @@ -24,7 +24,7 @@ This document outlines Serverpod's approach to testing code. It serves as a gui - Running a single test in isolation must produce the same result as running it alongside others. - **Exception to the rule:** e2e and integration tests. In scenarios where an external state (like a shared database) is involved, tests may require concurrency mode 1 to prevent interference. But each test should start and end in a clean state. -### 2. Clear and Descriptive Test Descriptions +### 2. Clear and descriptive test descriptions - **Test descriptions should be understandable without reading the test code.** - If a test fails, the description alone should make it clear what behavior went wrong @@ -39,7 +39,7 @@ This document outlines Serverpod's approach to testing code. It serves as a gui // Then a 403 Forbidden error is returned ``` -### 3. Focus on Single Responsibility +### 3. Focus on single responsibility - Each test should **only test one thing**. - **Avoid** bundling multiple independent checks into a single test. @@ -90,7 +90,7 @@ test('Add string to list and check index and length', () { \*Note: SourceSpanException is an object that describes a code error in a source file. See: \*[*https://api.flutter.dev/flutter/package-source\_span\_source\_span/SourceSpanException-class.html*](https://api.flutter.dev/flutter/package-source_span_source_span/SourceSpanException-class.html) -### 4. Pure Unit Testing +### 4. Pure unit testing - **Unit tests should avoid mocking and side effects.** - Production code should push side effects **up the call stack**, allowing tests to cover pure methods. @@ -108,7 +108,7 @@ Future fetchUserData() async { - Test `processResponse` directly without mocking the `httpClient`. -### 5. Implementation-Agnostic Tests +### 5. Implementation-agnostic tests - **Do not couple tests to implementation details.** - Tests should only break if the behavior changes, not when refactoring code. This may be referred to as black box testing. @@ -153,13 +153,13 @@ test('Given a user name when processed then toUpperCase is called', () { - In the bad example, the test verifies that an internal method (`_toUpperCase`) is called, coupling the test to the implementation. The good example validates only the output, ensuring the test focuses on behavior rather than internal details. -### 6. Immutable Test Philosophy +### 6. Immutable test philosophy - **Tests should rarely be modified.** - When functionality changes, **add new tests** and/or **remove obsolete ones**. - Avoid altering existing tests unless fixing bugs, e.g. invalid tests. -### 7. Simplicity Over Abstraction +### 7. Simplicity over abstraction - The explicit examples make it clear what is being tested and reduce the need to jump between methods. The abstracted logic example hides test behavior, making it harder to understand specific test scenarios at a glance. - **Avoid abstraction in tests.** @@ -199,12 +199,12 @@ test('Given a user name when processed then toUpperCase is called', () { }); ``` -### 7. Beneficial Abstractions - Test Builders +### 7. Beneficial abstractions - test builders - One abstraction we encourage is the **test builder pattern** for constructing test input objects. - Builders help create logical, valid objects while keeping tests simple and readable. -**Key Characteristics:** +**Key characteristics:** - Builder class names follow the pattern: `ObjectNameBuilder`. - Methods that set properties start with `with`, and return the builder instance (`this`). @@ -252,7 +252,7 @@ class UserBuilder { - Ensures tests continue to produce valid objects as the code evolves. - Simplifies object creation without requiring deep lifecycle knowledge. -## Final Thoughts +## Final thoughts Consistent application of these principles leads to a robust and maintainable codebase, fostering confidence in our product's reliability and scalability.