feat: Initial commit for Saqel Platform architecture, backend, Docker, and documentation
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
# Assertions
|
||||
|
||||
## Arrange, Act, Assert
|
||||
|
||||
Write each test in three parts: setup, one action, and assertions. Put one blank line between them so readers can identify each part without comments.
|
||||
|
||||
Keep each test self-contained. Do not use values created by another test.
|
||||
|
||||
## How to Find the Correct Assertion
|
||||
|
||||
First identify the subject of the check, then find an assertion designed for it. A subject-specific assertion identifies the incorrect value when the test fails.
|
||||
|
||||
1. Search Laravel's assertions for framework subjects such as responses, the database, sessions, models, queues, events, mail, and notifications.
|
||||
2. Fetch `https://docs.phpunit.de/en/13.3/assertions.html` for the assertions of PHPUnit for a plain value, a type, a format, or a shape.
|
||||
3. Build the check by hand only if no assertion exists for the subject.
|
||||
4. Confirm the name in the documentation before you use it. Do not write an assertion that you did not confirm.
|
||||
|
||||
Use the assertion in this table for each subject.
|
||||
|
||||
| Subject | Assertion to use |
|
||||
| --- | --- |
|
||||
| A return value, the state of an object, or a transformation of a value | `assertSame()`, or the assertion for the type |
|
||||
| An HTTP status, JSON, a session, or Inertia | a Laravel response assertion |
|
||||
| The state in the database | a Laravel database assertion |
|
||||
| The existence of a model | `assertModelExists($model)` rather than `assertDatabaseHas('users', ['id' => $user->id])` |
|
||||
|
||||
Use `assertSame()` and not `assertEquals()`, because `assertSame()` also compares the type.
|
||||
|
||||
Assert each fact once. Do not assert a 200 status before `assertSee`, because `assertSee` already shows that the page rendered.
|
||||
|
||||
## The Assertion with a Name for a Response
|
||||
|
||||
Use a named response assertion, such as `assertNotFound()`, rather than `assertStatus(404)`. A failure then identifies the broken contract. Laravel provides named assertions for commonly tested status codes.
|
||||
|
||||
Group the assertions for one subject together. Start a new group when the subject changes.
|
||||
|
||||
## Assert a Known Value
|
||||
|
||||
Write the expected value in the test, or calculate the expected value by a different method. Do not calculate the expected value with the logic of the implementation, because the test then passes when that logic is wrong.
|
||||
|
||||
```php
|
||||
// The test calculates the value with the logic of the implementation.
|
||||
$expected = now()->subHours(24)->floorSeconds(30)->toJson();
|
||||
$this->assertSame($expected, $from);
|
||||
|
||||
// The test sets a fixed input and asserts a known value.
|
||||
$this->travelTo('2025-01-01 00:00:00');
|
||||
$this->assertSame('2024-12-31T00:00:00.000000Z', $from);
|
||||
```
|
||||
|
||||
## Assert the Complete Result
|
||||
|
||||
A status code is not the complete result of a write operation. Assert each of the following if the operation changes it:
|
||||
|
||||
- the response or the return value
|
||||
- the state in the database
|
||||
- the jobs and the events that the operation dispatches
|
||||
- the notifications and the mail that the operation sends
|
||||
|
||||
On the failure path, assert that the operation makes none of these changes. A test that asserts only `assertOk()` passes even when the application saves no record.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Endpoint Tests
|
||||
|
||||
## How to Write the Test
|
||||
|
||||
Fetch `https://laravel.com/framework/docs/http-tests` for the request helpers, the authentication helpers, and the response assertions. Confirm the name before you use it, and do not guess an assertion.
|
||||
|
||||
Choose an assertion based on the subject of the check: the status, a header, a redirect, the JSON body, the session, a validation error, or the view. Laravel provides a named assertion for each subject that identifies the incorrect value.
|
||||
|
||||
## The Coverage of an Endpoint
|
||||
|
||||
Write a test for each applicable case:
|
||||
|
||||
- The request has missing or invalid authentication.
|
||||
- The request comes from a different tenant, team, or organization.
|
||||
- The user has an insufficient role or permission.
|
||||
- The request does not satisfy a route or scope constraint.
|
||||
- The request fails the validation.
|
||||
- The request is valid. Assert both the response and the persisted state.
|
||||
|
||||
Assert the application's actual behavior rather than a generic status code. An API returns `401` for a missing or invalid token, while a browser endpoint redirects to the sign-in route.
|
||||
|
||||
## The Isolation of a Tenant
|
||||
|
||||
Assert the status code returned for a cross-tenant request. Use `404` rather than `403` when one tenant must not learn that another tenant's record exists, because `403` confirms its existence.
|
||||
|
||||
## Test Authorization at the Policy Level
|
||||
|
||||
An HTTP test shows that the endpoint performs authorization. It cannot identify which mechanism refused the request because middleware, a policy, and a call to `abort()` can all return `403`.
|
||||
|
||||
- Assert the complete matrix of the permissions against the policy or the gate. A failure then names the rule that is not correct.
|
||||
- Write one HTTP test for one refused role, which shows that the endpoint calls the authorization.
|
||||
- Use the helper of the project that asserts the ability and the arguments of the gate, if such a helper exists.
|
||||
|
||||
## The Validation
|
||||
|
||||
- Write one test for each validation rule when each failure represents a separate contract.
|
||||
- Write one test with an empty payload to assert several required fields together.
|
||||
- Give the status code in the name of a test for an API.
|
||||
- Assert the text of the message that the user gets. A message that is present but wrong is a defect.
|
||||
- Use a data provider with the `#[DataProvider]` attribute for input values that need the same setup and the same assertions. Use the `#[TestWith]` attribute for a small set of values.
|
||||
|
||||
Send an input value that is not valid through the application, and assert the error. Do not assert that an array of rules contains a string, because that assertion tests the declaration and not the behavior. Use such an assertion only for a rule that no request can reach, and write the reason in the test.
|
||||
|
||||
### Which Layer Owns Which Case
|
||||
|
||||
The rule-class test owns the matrix of values that pass and fail. The endpoint test proves that the endpoint applies the rule and that the user receives the message.
|
||||
|
||||
When both tests contain the matrix, move it to the rule-class test and retain one case in the endpoint test. Never remove the last case, because the rule-class test still passes if the request omits the rule. The same division applies to policies, scopes, and other classes called by a request.
|
||||
@@ -0,0 +1,35 @@
|
||||
# How to Find Test Framework Features
|
||||
|
||||
PHPUnit and Laravel provide features for most testing needs. Find an existing feature before implementing the behavior by hand.
|
||||
|
||||
- Give `search-docs` the capability you need rather than the name of a method you remember. It returns Laravel testing documentation for the installed version.
|
||||
- Fetch `https://phpunit.de/documentation.html` for version-specific PHPUnit attributes, assertions, and command-line options.
|
||||
- If a search returns no results, tell the user that the installed version does not provide the feature. Do not write an API that you have not confirmed.
|
||||
|
||||
Search for a feature in this table before you write the code by hand.
|
||||
|
||||
| Work that you need | Term to search for |
|
||||
| --- | --- |
|
||||
| Run one test method with many input values | data provider, `#[DataProvider]`, `#[TestWith]` |
|
||||
| Run one test only after another test passes | `#[Depends]` |
|
||||
| Select or skip a set of tests in one run | `#[Group]`, `--group`, `--exclude-group` |
|
||||
| Skip a test on a version or on a missing extension | `#[RequiresPhp]`, `#[RequiresPhpExtension]` |
|
||||
| Find a test that depends on the order of the run | `--order-by=random` |
|
||||
| Reduce the time of a slow suite | ParaTest, `--cache-result` |
|
||||
| Stop the run at the first failure while you debug | `--stop-on-failure`, `--filter` |
|
||||
|
||||
## The Assertions of Laravel
|
||||
|
||||
Laravel provides assertions for each part of the framework. Fetch `https://laravel.com/framework/docs/testing` for the complete list, and search for an assertion before building a check by hand. Examples include `assertDatabaseHas()`, `assertModelExists()`, `assertSoftDeleted()`, response assertions such as `assertRedirectToRoute()` and `assertJsonPath()`, and fake assertions such as `Queue::assertPushed()` and `Notification::assertSentTo()`.
|
||||
|
||||
A hand-built check fails with `false is not true`, which identifies nothing. A framework assertion names the incorrect table, value, or response, so the failure indicates what to fix.
|
||||
|
||||
```php
|
||||
// The failure says that false is not true.
|
||||
// Instead of this
|
||||
$this->assertTrue(User::where('email', 'taylor@laravel.com')->exists());
|
||||
|
||||
// Use this
|
||||
// The failure names the table and the attributes that it did not find.
|
||||
$this->assertDatabaseHas('users', ['email' => 'taylor@laravel.com']);
|
||||
```
|
||||
@@ -0,0 +1,52 @@
|
||||
# Fakes, Mocks, and Determinism
|
||||
|
||||
Tests that depend on actual time, randomness, sleeping, or network calls can fail for reasons unrelated to the code under test. Control all four.
|
||||
|
||||
## How to Isolate a Dependency
|
||||
|
||||
Fetch `https://laravel.com/framework/docs/mocking` for Laravel's fakes, facade doubles, and fake assertions. Confirm each name before using it.
|
||||
|
||||
Identify the dependency, then choose the first applicable option. A framework fake preserves the real code path, while a mock replaces the dependency.
|
||||
|
||||
1. Use framework fakes for facades such as events, queues, mail, notifications, storage, the HTTP client, time, and sleep.
|
||||
2. Use the fake implementation of the project for a service of the project, if such a fake exists.
|
||||
3. Use a mock for a container-resolved contract only when the real implementation leaves the process or is nondeterministic.
|
||||
4. Use the real implementation for everything else, including the database.
|
||||
|
||||
## The Fakes
|
||||
|
||||
- Create each fake inside the test method that needs it. Do not create fakes in `setUp()`.
|
||||
- Pass class names to `Event::fake()` and `Queue::fake()` when you know which classes the code dispatches. A fake without class names can hide an unexpected dispatch.
|
||||
- Use a fake without class names only when the test asserts the complete result, including a call to `assertNothingPushed()`.
|
||||
- Write one assertion for each fake. The assertion states that the code dispatches the item, or that the code does not dispatch the item.
|
||||
- Assert the data of a job or of an event if that data is part of the behavior.
|
||||
- Use `Exceptions::fake()` to assert that the application reports the correct exception. Do not use `withoutExceptionHandling()`, because it changes the response under test.
|
||||
|
||||
Create prerequisite factory records before calling `Event::fake()`. Factories use model events, such as a `creating` hook that generates a UUID, and a fake without class names suppresses those events and can produce an invalid model. Call the fake first only when a factory event is under test, and pass that event's class name.
|
||||
|
||||
## The Mocks
|
||||
|
||||
Use `shouldReceive()` before the action to declare an expectation. Use `shouldHaveReceived()` after the action for a spy. Use `Mockery::on()` or `withArgs()` if an equality check cannot state the expected argument, such as a check of one field of a value object.
|
||||
|
||||
Use `$this->mock(Contract::class)` to put a mock in the container. Do not build a PHPUnit mock for a class that Mockery can double, because the project uses Mockery.
|
||||
|
||||
## The Outbound HTTP
|
||||
|
||||
Call `Http::preventStrayRequests()`. Any request without a matching fake then fails without reaching the network.
|
||||
|
||||
Fake the exact endpoint used by each test. Do not call `Http::fake()` without an endpoint because it accepts unexpected requests and can hide defects.
|
||||
|
||||
## The Time and the Randomness
|
||||
|
||||
- Freeze the time or move the time in each test that depends on a date, a period, or a timestamp.
|
||||
- Use the framework helpers `$this->freezeTime()`, `$this->travelTo()`, `$this->travel()`, and `$this->travelBack()`. Do not call `Carbon::setTestNow()`.
|
||||
- Use `Str::createRandomStringsUsing()` to fix a generated string, if the test asserts an identifier or a slug.
|
||||
- Use `Sleep::fake()` instead of a real sleep, and assert the sleeps that the code requests.
|
||||
- Restore the time and the randomness after each test, if the suite does not restore them for every test.
|
||||
|
||||
## The Database
|
||||
|
||||
- Run the real query against the real records in the test database. Do not mock the query builder, because the test then asserts the mock.
|
||||
- Assert the exact keys of `toArray()` if the shape of the serialized model is a contract. The test then fails when the model exposes a new attribute.
|
||||
- Test application behavior caused by the schema, such as deleting dependent records through a cascade. Do not test the database engine's cascade implementation.
|
||||
- Use `LazilyRefreshDatabase` instead of `RefreshDatabase`. A test that does not use the database then does not run the migrations.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Naming and Structure
|
||||
|
||||
## File Layout
|
||||
|
||||
- Name each test file `{ClassName}Test.php`.
|
||||
- Place each test file at the same relative path as the class under test. The class `app/Actions/DeleteTeam.php` gets the test `tests/Unit/Actions/DeleteTeamTest.php`.
|
||||
- Follow the project's convention for fixture files. If none exists, put fixtures in `tests/Fixtures/` and load them by path.
|
||||
- Move large literal values out of the test body and into fixture files.
|
||||
|
||||
## The Test Class and the Test Methods
|
||||
|
||||
- Extend the base `TestCase` of the project in each test class.
|
||||
- Give each test method the prefix `test_`, or add the `#[Test]` attribute to the method. Use the convention of the other files in the same directory.
|
||||
|
||||
## The Names of the Tests
|
||||
|
||||
The name of a test method is a specification. Separate the words with underscores. State the user-visible result and the condition that causes it.
|
||||
|
||||
- Name the behavior, and not the method under test. The file name already gives the class.
|
||||
- Give the exact status code in the name of a test for an API error.
|
||||
- Do not write `given`, `when`, or `then` in the name.
|
||||
|
||||
```php
|
||||
public function test_unauthenticated_request_redirects_to_login(): void { ... }
|
||||
public function test_returns_401_when_no_token_is_provided(): void { ... }
|
||||
public function test_valid_payload_creates_record_and_returns_201(): void { ... }
|
||||
```
|
||||
|
||||
Use a verb that describes a result, such as `returns`, `renders`, `creates`, `dispatches`, `rejects`, `forbids`, `falls back`, or `does not`.
|
||||
|
||||
Do not write `test_store()`, `test_it_works()`, or `test_validation()`, because none of them gives a result.
|
||||
|
||||
## Grouping
|
||||
|
||||
Write one test class for each class under test. Write a separate test class if one file covers separate actions in a lifecycle, such as `StoreOrderControllerTest` and `UpdateOrderControllerTest`.
|
||||
|
||||
Use the `#[Group]` attribute to mark the tests that a run must select or must skip. Do not use a group to give structure to a file, because a class gives the structure.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Test Suite Performance
|
||||
|
||||
These settings apply to the project and CI, not to individual tests. Read `rules/isolation.md` for choices within a test.
|
||||
|
||||
Fetch `https://docs.phpunit.de/en/13.3/` for PHPUnit options that make test runs faster.
|
||||
Verify each flag in the documentation before adding it to CI.
|
||||
|
||||
Measure before changing a setting. Find the slow test first, and apply a project-wide setting only after identifying the costly work.
|
||||
|
||||
## The Environment
|
||||
|
||||
- Set `BCRYPT_ROUNDS=4` in `.env.testing` or in `phpunit.xml`. The default value is 12, and the hash then takes most of the time of each test that signs a user in.
|
||||
- Disable XDebug. Disable pcov also, unless the run needs the coverage.
|
||||
- Disable packages that perform work on every request in the test environment. Examples are Pulse, Telescope, and Nightwatch.
|
||||
- Use the `WithCachedConfig` and `WithCachedRoutes` traits, so the run does not parse the configuration and the routes for every test.
|
||||
- Call `withoutVite()`, or `withoutMix()`, so the framework does not resolve a built asset.
|
||||
|
||||
## The Global Fakes
|
||||
|
||||
Put these three calls in the `setUp()` of the base `TestCase` of the project:
|
||||
|
||||
- `Http::preventStrayRequests()`, because one request that reaches the network can slow the suite. This catches requests made through Laravel's HTTP client. Check direct Guzzle and cURL usage separately.
|
||||
- `Sleep::fake(syncWithCarbon: true)`, so a retry and a backoff do not sleep.
|
||||
- `Exceptions::fake()`, so the suite does not report an exception to an external service.
|
||||
|
||||
## How to Run the Suite in Parallel
|
||||
|
||||
Run `php artisan test --parallel`, which uses ParaTest, to spread tests across the machine's CPU cores. Add `--processes=N` if the default count is unsuitable for the machine or CI.
|
||||
|
||||
A parallel run gives each process a separate database. Tests must meet these conditions; a test that fails only in parallel breaks one of them:
|
||||
|
||||
- The test creates each record that it reads. It does not read a record that another test creates.
|
||||
- The test does not depend on the order of the run.
|
||||
- The test does not share a file, a cache key, or a queue with another test. Give each process a separate name for such a resource.
|
||||
|
||||
## How to Find a Slow Test
|
||||
|
||||
Run `php artisan test --profile` to list the slowest tests. Start with the ten slowest tests, because the same cause often applies to the complete suite.
|
||||
|
||||
If the cause of a slow test is unclear, add an event listener or temporary log entry to identify its work.
|
||||
|
||||
## Common Errors
|
||||
|
||||
- The run loads XDebug for a test that does not need it.
|
||||
- `BCRYPT_ROUNDS` keeps the default value, because the project has no `.env.testing`.
|
||||
- The code under test calls the real `sleep()`, and `Sleep::fake()` then does not help.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Reviewing Tests
|
||||
|
||||
Check every item in this file. A passing test may still provide no value. For each test, identify the defect it would catch.
|
||||
|
||||
Report each finding. Do not delete or rewrite a test without the user's approval. When an issue appears throughout the suite as a convention, report the pattern once rather than every affected file.
|
||||
|
||||
## The Value of the Test
|
||||
|
||||
- [ ] Each test covers observable behavior or an application contract, and passes after a change to the implementation that keeps the behavior.
|
||||
- [ ] Each tested declaration is exercised through behavior, and no test asserts the behavior of the framework. A test of what this project configures, such as a relation with a constraint, a cast, or a scope, belongs to this project.
|
||||
- [ ] Each test detects a distinct defect that no other test covers. A duplicate shrinks at the higher layer to the one case that proves the wiring.
|
||||
- [ ] Every changed decision and each applicable high-value failure mode has coverage.
|
||||
|
||||
## Names and Structure
|
||||
|
||||
- [ ] Each file has the name `{ClassName}Test.php` and the relative path of the class under test.
|
||||
- [ ] Each name states a result, the condition that causes it, and the status code for an API error.
|
||||
- [ ] Each test class extends the base `TestCase` of the project, and each file uses either the prefix `test_` or the `#[Test]` attribute consistently.
|
||||
|
||||
## The Coverage
|
||||
|
||||
- [ ] HTTP tests cover authentication, authorization, role, scope, and validation when applicable.
|
||||
- [ ] A request for a record of a different tenant gets a status code that does not confirm that the record exists.
|
||||
- [ ] The complete permission matrix belongs in policy tests, not controller tests.
|
||||
- [ ] Each validation rule has one test that asserts the user-visible message. When a unit test owns a matrix, reduce duplicate higher-level coverage to one case rather than deleting it.
|
||||
- [ ] Rendered user input and each dynamic part of a query have a security test.
|
||||
|
||||
## The Data and the Determinism
|
||||
|
||||
- [ ] Each test creates its mutable records directly or through a helper that it calls, and every created record arranges the behavior or supports an assertion.
|
||||
- [ ] `setUp()` holds configuration only.
|
||||
- [ ] Each factory state and each relationship gives the meaning of the data.
|
||||
- [ ] Each call to `make()` is in a test that does not need the database.
|
||||
- [ ] Time, randomness, sleep, and outbound HTTP are controlled.
|
||||
- [ ] Each test passes alone, and passes in the complete suite in any order.
|
||||
|
||||
## The Assertions
|
||||
|
||||
- [ ] Each expected value is a known value, and the test does not calculate the value with the logic of the implementation.
|
||||
- [ ] Each test of a write operation asserts the response, the state in the database, and the side effects.
|
||||
- [ ] Each fake has one assertion, and gives the class names unless the test asserts the complete result.
|
||||
- [ ] Each group of assertions stays on one subject, and each comparison uses `assertSame()`.
|
||||
|
||||
## The Defects to Report
|
||||
|
||||
A review can find defects in the code rather than the tests. Report each defect below, and do not write a test that codifies it as correct behavior.
|
||||
|
||||
- [ ] A method with no body.
|
||||
- [ ] A policy that exists, but that no action calls.
|
||||
- [ ] A write action with no validation.
|
||||
- [ ] A status code or a response shape that is different from the shape of a similar endpoint.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Security Tests
|
||||
|
||||
Test each security boundary where user input affects authorization, rendered output, or query construction. A defect at such a boundary can be difficult to detect because the feature may continue to work.
|
||||
|
||||
Write a test for each of these cases:
|
||||
|
||||
- **Cross-tenant access.** Request a record of a different tenant, team, or organization. Read `rules/endpoint-tests.md` for why the response should be `404` rather than `403`.
|
||||
- **Each unprivileged role.** Use a data provider over the roles that the endpoint must refuse.
|
||||
- **Escaping user-provided content.** Test escaping in HTML and mail. Include names and every free-text field a template renders. Assert that dangerous characters are escaped and the raw value is absent. Do not assert an exact entity for a quote, because Markdown and mail CSS inliners may decode it.
|
||||
- **Injection into dynamic query components.** Examples include sort columns, filter fields, and sort directions.
|
||||
- **An unexpected key** in a payload or configuration array. A merge that accepts every key can set an attribute the user must not control.
|
||||
|
||||
```php
|
||||
public function test_escapes_dangerous_content_in_the_notification(): void
|
||||
{
|
||||
$organization = Organization::factory()->make([
|
||||
'name' => "O'Reilly <script>alert('xss')</script>",
|
||||
]);
|
||||
|
||||
$content = (new QuotaApproaching($organization, 80))->toMail()->render();
|
||||
|
||||
$this->assertStringContainsString('<script>', $content);
|
||||
$this->assertStringNotContainsString("<script>alert('xss')</script>", $content);
|
||||
}
|
||||
```
|
||||
|
||||
Laravel provides defenses against mass assignment, unauthorized access, and unescaped output. Test that the application applies the appropriate defense to each attribute, route, and template.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Factories and Test Data
|
||||
|
||||
## Each Test Makes Its Own Data
|
||||
|
||||
Create mutable records inside each test or through a private helper that the test calls. This keeps setup visible and lets each test select its factory state.
|
||||
|
||||
Use `setUp()` only for configuration that applies to every test in the class. Do not create records in it, because its objects remain in memory until the suite ends.
|
||||
|
||||
## Record Construction
|
||||
|
||||
- Use `create()` if the test needs the record in the database.
|
||||
- Use `make()` only if the test does not need the database. Examples include rendering a notification and testing a value object's behavior.
|
||||
- Use a named factory state instead of a raw attribute. `User::factory()->unverified()->create()` gives the state meaning; `create(['email_verified_at' => null])` gives only its value.
|
||||
- Use `for()` or the relationship helper of the project to declare the owner of a record.
|
||||
- Use `recycle()` if several records must share one parent record.
|
||||
- Use `sequence()` if several records need different attributes.
|
||||
|
||||
```php
|
||||
$organization = Organization::factory()->onPlan(BillingPlan::PRO)->create();
|
||||
|
||||
$environment = Environment::factory()->recycle($organization)->create();
|
||||
|
||||
$organizations = Organization::factory()
|
||||
->count(3)
|
||||
->sequence(
|
||||
['created_at' => now()->setSeconds(30)],
|
||||
['created_at' => now()->setSeconds(1)],
|
||||
)
|
||||
->create();
|
||||
```
|
||||
|
||||
Create only the records required to arrange the behavior or support an assertion.
|
||||
|
||||
## The Data Providers
|
||||
|
||||
Use a data provider when the setup, test body, and assertions remain the same across input values.
|
||||
|
||||
```php
|
||||
public static function nonAdminRoles(): array
|
||||
{
|
||||
return collect(Role::cases())
|
||||
->reject(fn (Role $role): bool => $role === Role::ADMIN)
|
||||
->mapWithKeys(fn (Role $role): array => [$role->value => [$role]])
|
||||
->all();
|
||||
}
|
||||
|
||||
#[DataProvider('nonAdminRoles')]
|
||||
public function test_forbids_roles_other_than_admin(Role $role): void
|
||||
{
|
||||
$this->actingAs(User::factory()->hasOrganization($role)->create())
|
||||
->post('/settings')
|
||||
->assertForbidden();
|
||||
}
|
||||
```
|
||||
|
||||
Declare each data provider method as `public static`.
|
||||
|
||||
Use parameterized tests for:
|
||||
|
||||
- the cases of an enum
|
||||
- the roles and the plans
|
||||
- the boundary values
|
||||
- the input values that are not valid in the same way
|
||||
- the pairs of an input value and an output value
|
||||
|
||||
Write separate tests if the cases need a different setup, a different behavior, or different assertions. One test function with a branch in the body is two tests in one function.
|
||||
|
||||
Give each data-provider case a key that states the difference. A failure then identifies the case without requiring you to count positions.
|
||||
Reference in New Issue
Block a user