Architecture Testing
Architecture testing enables you to specify expectations that test whether your application adheres to a set of architectural rules, helping you maintain a clean and sustainable codebase. The expectations are determined by either relative namespaces, fully qualified namespaces, or function names.
Here is an example of how you can define an architectural rule:
1arch() 2 ->expect('App') 3 ->toUseStrictTypes() 4 ->not->toUse(['die', 'dd', 'dump']); 5 6arch() 7 ->expect('App\Models') 8 ->toBeClasses() 9 ->toExtend('Illuminate\Database\Eloquent\Model')10 ->toOnlyBeUsedIn('App\Repositories')11 ->ignoring('App\Models\User');12 13arch()14 ->expect('App\Http')15 ->toOnlyBeUsedIn('App\Http');16 17arch()->preset()->php();18arch()->preset()->security()->ignoring('md5');
Now, let's dive into the various methods and modifiers available for architectural testing. In this section, you will learn:
- Expectations: Allows to specify granular architectural rules.
- Presets: Allows to use predefined sets of granular architectural rules.
- Modifiers: To exclude or ignore certain types of files, classes, functions or lines of code.
Expectations
Granular expectations allow you to define specific architectural rules for your application. Here are the available expectations:
-
toBeAbstract()
-
toBeClasses()
-
toBeEnums()
-
toBeIntBackedEnums()
-
toBeInterfaces()
-
toBeInvokable()
-
toBeFinal()
-
toBeReadonly()
-
toBeStringBackedEnums()
-
toBeTraits()
-
toBeUsed()
-
toBeUsedIn()
-
toExtend()
-
toExtendNothing()
-
toImplement()
-
toImplementNothing()
-
toHaveMethodsDocumented()
-
toHavePropertiesDocumented()
-
toHaveAttribute()
-
toHaveFileSystemPermissions()
-
toHaveLineCountLessThan()
-
toHaveMethod()
-
toHaveMethods()
-
toHavePrivateMethodsBesides()
-
toHavePrivateMethods()
-
toHaveProtectedMethodsBesides()
-
toHaveProtectedMethods()
-
toHavePublicMethodsBesides()
-
toHavePublicMethods()
-
toHavePrefix()
-
toHaveSuffix()
-
toHaveConstructor()
-
toHaveDestructor()
-
toOnlyImplement()
-
toOnlyUse()
-
toOnlyBeUsedIn()
-
toUse()
-
toUseStrictEquality()
-
toUseTrait()
-
toUseTraits()
-
toUseNothing()
-
toUseStrictTypes()
toBeAbstract()
The toBeAbstract()
method may be used to ensure that all classes within a given namespace are abstract.
1arch('app')2 ->expect('App\Models')3 ->toBeAbstract();
toBeClasses()
The toBeClasses()
method may be used to ensure that all files within a given namespace are classes.
1arch('app')2 ->expect('App\Models')3 ->toBeClasses();
toBeEnums()
The toBeEnums()
method may be used to ensure that all files within a given namespace are enums.
1arch('app')2 ->expect('App\Enums')3 ->toBeEnums();
toBeIntBackedEnums()
The toBeIntBackedEnums()
method may be used to ensure that all enums within a specified namespace are int-backed.
1arch('app')2 ->expect('App\Enums')3 ->toBeIntBackedEnums();
toBeInterfaces()
The toBeInterfaces()
method may be used to ensure that all files within a given namespace are interfaces.
1arch('app')2 ->expect('App\Contracts')3 ->toBeInterfaces();
toBeInvokable()
The toBeInvokable()
method may be used to ensure that all files within a given namespace are invokable.
1arch('app')2 ->expect('App\Actions')3 ->toBeInvokable();
toBeTraits()
The toBeTraits()
method may be used to ensure that all files within a given namespace are traits.
1arch('app')2 ->expect('App\Concerns')3 ->toBeTraits();
toBeFinal()
The toBeFinal()
method may be used to ensure that all classes within a given namespace are final.
1arch('app')2 ->expect('App\ValueObjects')3 ->toBeFinal();
Note that, typically this expectation is used in combination with the classes()
modifier to ensure that all classes within a given namespace are final.
1arch('app')2 ->expect('App')3 ->classes()4 ->toBeFinal();
toBeReadonly()
The toBeReadonly()
method may be used to ensure that certain classes are immutable and cannot be modified at runtime.
1arch('app')2 ->expect('App\ValueObjects')3 ->toBeReadonly();
Note that, typically this expectation is used in combination with the classes()
modifier to ensure that all classes within a given namespace are readonly.
1arch('app')2 ->expect('App')3 ->classes()4 ->toBeReadonly();
toBeStringBackedEnums()
The toBeStringBackedEnums()
method may be used to ensure that all enums within a specified namespace are string-backed.
1arch('app')2 ->expect('App\Enums')3 ->toBeStringBackedEnums();
toBeUsed()
The not
modifier, when combined with the toBeUsed()
method, enables you to verify that certain classes or functions are not being utilized by your application.
1arch('globals')2 ->expect(['dd', 'dump'])3 ->not->toBeUsed();4 5arch('facades')6 ->expect('Illuminate\Support\Facades')7 ->not->toBeUsed();
toBeUsedIn()
By combining the not
modifier with the toBeUsedIn()
method, you can restrict specific classes and functions from being used within a given namespace.
1arch('globals')2 ->expect('request')3 ->not->toBeUsedIn('App\Domain');4 5arch('globals')6 ->expect('Illuminate\Http')7 ->not->toBeUsedIn('App\Domain');
toExtend()
The toExtend()
method may be used to ensure that all classes within a given namespace extend a specific class.
1arch('app')2 ->expect('App\Models')3 ->toExtend('Illuminate\Database\Eloquent\Model');
toExtendNothing()
The toExtendNothing()
method may be used to ensure that all classes within a given namespace do not extend any class.
1arch('app')2 ->expect('App\ValueObjects')3 ->toExtendNothing();
toImplement()
The toImplement()
method may be used to ensure that all classes within a given namespace implement a specific interface.
1arch('app')2 ->expect('App\Jobs')3 ->toImplement('Illuminate\Contracts\Queue\ShouldQueue');
toImplementNothing()
The toImplementNothing()
method may be used to ensure that all classes within a given namespace do not implement any interface.
1arch('app')2 ->expect('App\ValueObjects')3 ->toImplementNothing();
toHaveMethodsDocumented()
The toHaveMethodsDocumented()
method may be used to ensure that all methods within a given namespace are documented.
1arch('app')2 ->expect('App')3 ->toHaveMethodsDocumented();
toHavePropertiesDocumented()
The toHavePropertiesDocumented()
method may be used to ensure that all properties within a given namespace are documented.
1arch('app')2 ->expect('App')3 ->toHavePropertiesDocumented();
toHaveAttribute()
The toHaveAttribute()
method may be used to ensure that a certain class has a specific attribute.
1arch('app')2 ->expect('App\Console\Commands')3 ->toHaveAttribute('Symfony\Component\Console\Attribute\AsCommand');
toHaveFileSystemPermissions()
The toHaveFileSystemPermissions()
method may be used to ensure that all files within a given namespace have specific file system permissions.
1arch('app')2 ->expect('App')3 ->not->toHaveFileSystemPermissions('0777');
toHaveLineCountLessThan()
The toHaveLineCountLessThan()
method may be used to ensure that all files within a given namespace have a line count less than a specified value.
1arch('app')2 ->expect('App\Models')3 ->toHaveLineCountLessThan(100);
toHaveMethod()
The toHaveMethod()
method may be used to ensure that a certain class has a specific method.
1arch('app')2 ->expect('App\Http\Controllers\HomeController')3 ->toHaveMethod('index');
toHaveMethods()
The toHaveMethods()
method may be used to ensure that a certain class has specific methods.
1arch('app')2 ->expect('App\Http\Controllers\HomeController')3 ->toHaveMethods(['index', 'show']);
toHavePrivateMethodsBesides()
The toHavePrivateMethodsBesides()
method may be used to ensure that a certain class does not have any private methods besides the specified ones.
1arch('app')2 ->expect('App\Services\PaymentService')3 ->not->toHavePrivateMethodsBesides(['doPayment']);
toHavePrivateMethods()
The toHavePrivateMethods()
method may be used to ensure that a certain class does not have any private methods.
1arch('app')2 ->expect('App\Services\PaymentService')3 ->not->toHavePrivateMethods();
toHaveProtectedMethodsBesides()
The toHaveProtectedMethodsBesides()
method may be used to ensure that a certain class does not have any protected methods besides the specified ones.
1arch('app')2 ->expect('App\Services\PaymentService')3 ->not->toHaveProtectedMethodsBesides(['doPayment']);
toHaveProtectedMethods()
The toHaveProtectedMethods()
method may be used to ensure that a certain class does not have any protected methods.
1arch('app')2 ->expect('App\Services\PaymentService')3 ->not->toHaveProtectedMethods();
toHavePublicMethodsBesides()
The toHavePublicMethodsBesides()
method may be used to ensure that a certain class does not have any public methods besides the specified ones.
1arch('app')2 ->expect('App\Services\PaymentService')3 ->not->toHavePublicMethodsBesides(['charge', 'refund']);
toHavePublicMethods()
The toHavePublicMethods()
method may be used to ensure that a certain class does not have any public methods.
1arch('app')2 ->expect('App\Services\PaymentService')3 ->not->toHavePublicMethods();
toHavePrefix()
The toHavePrefix()
method may be used to ensure that all files within a given namespace have a specific prefix.
1arch('app')2 ->expect('App\Helpers')3 ->not->toHavePrefix('Helper');
toHaveSuffix()
The toHaveSuffix()
method may be used to ensure that all files within a given namespace have a specific suffix.
1arch('app')2 ->expect('App\Http\Controllers')3 ->toHaveSuffix('Controller');
toHaveConstructor()
This toHaveConstructor()
method may be used to ensure that all files within a given namespace have a __construct
method.
1arch('app')2 ->expect('App\ValueObjects')3 ->toHaveConstructor();
toHaveDestructor()
This toHaveDestructor()
method may be used to ensure that all files within a given namespace have a __destruct
method.
1arch('app')2 ->expect('App\ValueObjects')3 ->toHaveDestructor();
toOnlyImplement()
The toOnlyImplement()
method may be used to ensure that certain classes are restricted to implementing specific interfaces.
1arch('app')2 ->expect('App\Responses')3 ->toOnlyImplement('Illuminate\Contracts\Support\Responsable');
toOnlyUse()
The toOnlyUse()
method may be used to guarantee that certain classes are restricted to utilizing specific functions or classes. For example, you may ensure your models are streamlined and solely dependent on the Illuminate\Database
namespace, and not, for instance, dispatching queued jobs or events.
1arch('models')2 ->expect('App\Models')3 ->toOnlyUse('Illuminate\Database');
toOnlyBeUsedIn()
The toOnlyBeUsedIn()
method enables you to limit the usage of a specific class or set of classes to only particular parts of your application. For instance, you can use this method to confirm that your models are only used by your repositories and not by controllers or service providers.
1arch('models')2 ->expect('App\Models')3 ->toOnlyBeUsedIn('App\Repositories');
toUse()
By combining the not
modifier with the toUse()
method, you can indicate that files within a given namespace should not use specific functions or classes.
1arch('globals')2 ->expect('App\Domain')3 ->not->toUse('request');4 5arch('globals')6 ->expect('App\Domain')7 ->not->toUse('Illuminate\Http');
toUseStrictEquality()
The toUseStrictEquality()
method may be used to ensure that all files within a given namespace use strict equality. In other words, the ===
operator is used instead of the ==
operator.
1arch('models')2 ->expect('App')3 ->toUseStrictEquality();
Or, if you rather want to ensure that all files within a given namespace do not use strict equality, you may use the not
modifier.
1arch('models')2 ->expect('App')3 ->not->toUseStrictEquality();
toUseTrait()
The toUseTrait()
method may be used to ensure that all files within a given namespace use a specific trait.
1arch('models')2 ->expect('App\Models')3 ->toUseTrait('Illuminate\Database\Eloquent\SoftDeletes');
toUseTraits()
The toUseTraits()
method may be used to ensure that all files within a given namespace use specific traits.
1arch('models')2 ->expect('App\Models')3 ->toUseTraits(['Illuminate\Database\Eloquent\SoftDeletes', 'App\Concerns\CustomTrait']);
toUseNothing()
If you want to indicate that particular namespaces or classes should not have any dependencies, you can utilize the toUseNothing()
method.
1arch('value objects')2 ->expect('App\ValueObjects')3 ->toUseNothing();
toUseStrictTypes()
The toUseStrictTypes()
method may be used to ensure that all files within a given namespace utilize strict types.
1arch('app')2 ->expect('App')3 ->toUseStrictTypes();
Presets
Sometimes, writing arch expectations from scratch can be time-consuming, specifically when working on a new project, and you just want to ensure that the basic architectural rules are met.
Presets are predefined sets of granular expectations that you can use to test your application's architecture.
php
The php
preset is a predefined set of expectations that can be used on any php project. It's not coupled with any framework or library.
It avoids the usage of die
, var_dump
, and similar functions, and ensures you are not using deprecated PHP functions.
1arch()->preset()->php();
You may find all the expectations included in the php
preset below in our source code.
security
The security
preset is a predefined set of expectations that can be used on any php project. It's not coupled with any framework or library.
It ensures you are not using code that could lead to security vulnerabilities, such as eval
, md5
, and similar functions.
1arch()->preset()->security();
You may find all the expectations included in the security
preset below in our source code.
laravel
The laravel
preset is a predefined set of expectations that can be used on Laravel projects.
It ensures you project's structure is following the well-known Laravel conventions, such as controllers only have index
, show
, create
, store
, edit
, update
, destroy
as public methods and are always suffixed with Controller
and so on.
1arch()->preset()->laravel();
You may find all the expectations included in the laravel
preset below in our source code.
strict
The strict
preset is a predefined set of expectations that can be used on any php project. It's not coupled with any framework or library.
It ensures you are using strict types in all your files, that all your classes are final, and more.
1arch()->preset()->strict();
You may find all the expectations included in the strict
preset below in our source code.
relaxed
The relaxed
preset is a predefined set of expectations that can be used on any php project. It's not coupled with any framework or library.
It is the opposite of the strict
preset, ensuring you are not using strict types in all your files, that all your classes are not final, and more.
1arch()->preset()->relaxed();
You may find all the expectations included in the relaxed
preset below in our source code.
custom
Typically you don't need to use the custom
preset, as you can use the arch()
method to write your granular expectations. However, if you want to create your own preset, you can use the custom
preset.
This may be useful if you have a set of expectations that you use frequently across multiple projects, or if you are plugin author and want to provide a set of expectations for your users.
1pest()->preset('ddd', function () {2 return [3 expect('Infrastructure')->toOnlyBeUsedIn('Application'),4 expect('Domain')->toOnlyBeUsedIn('Application'),5 ];6});
With the preset
method, you may have access to the application PSR-4 namespaces on the first argument of your closure's callback.
1pest()->preset('silex', function (array $userNamespaces) {2 dump($userNamespaces); // ['App\\']3});
Modifiers
Sometimes, you may want to apply the given expectation but excluding certain types of files, or ignoring certain classes, functions, or specific lines of code. For that, you may use the following methods:
ignoring()
When defining your architecture rules, you can use the ignoring()
method to exclude certain namespaces or classes that would otherwise be included in the rule definition.
1arch()2 ->preset()3 ->php()4 ->ignoring('die');5 6arch()7 ->expect('Illuminate\Support\Facades')8 ->not->toBeUsed()9 ->ignoring('App\Providers');
In some cases, certain components may not be regarded as "dependencies" as they are part of the native PHP library. To customize the definition of "native" code and exclude it during testing, Pest allows you to specify what to ignore.
For example, if you do not want to consider Laravel a "dependency", you can use the arch()
method inside the beforeEach()
function to disregard any code within the "Illuminate" namespace. This approach allows you to focus only on the actual dependencies of your application.
1// tests/Pest.php2pest()->beforeEach(function () {3 $this->arch()->ignore([4 'Illuminate',5 ])->ignoreGlobalFunctions();6});
classes()
The classes()
modifier allows you to restrict the expectation to only classes.
1arch('app')2 ->expect('App')3 ->classes()4 ->toBeFinal();
enums()
The enums()
modifier allows you to restrict the expectation to only enums.
1arch('app')2 ->expect('App\Models')3 ->enums()4 ->toOnlyBeUsedIn('App\Models');
interfaces()
The interfaces()
modifier allows you to restrict the expectation to only interfaces.
1arch('app')2 ->expect('App')3 ->interfaces()4 ->toExtend('App\Contracts\Contract');
traits()
The traits()
modifier allows you to restrict the expectation to only traits.
1arch('app')2 ->expect('App')3 ->traits()4 ->toExtend('App\Traits\Trait');
In this section, you have learned how to perform architectural testing, ensuring that your application or library's architecture meets a specified set of architectural requirements. Next, have you ever wondered how to test the performance of your code? Let's explore Stress Testing.