Coding Standard, Dependency and Legacy under control
Static tests in Magento 2 catch exactly the class of problems that neither PHPStan nor PHPUnit see: violations of the Magento Coding Standard, undeclared dependencies between modules, and legacy patterns quietly reintroduced into the codebase. Whoever takes the suite under dev/tests/static seriously prevents exactly the kind of structural failure that only becomes visible at the next major upgrade or when module load order changes.
Table of Contents
- 1. Static tests in Magento 2: scope and boundaries
- 2. Magento Coding Standard: the phpcs ruleset and sniff categories
- 3. Setting up phpcs.xml and running the Magento Coding Standard
- 4. The Dependency test: catching undeclared module dependencies
- 5. Declaring module sequences correctly in module.xml
- 6. Legacy test: detecting outdated Magento patterns automatically
- 7. Structure of dev/tests/static/testsuite and how to run it
- 8. Wiring static tests into a CI pipeline
- 9. Comparison: Coding Standard, Dependency Test, Legacy Test and PHPStan
- 10. Summary
- 11. FAQ
1. Static tests in Magento 2: scope and boundaries
Static tests in Magento 2 are not a single file or a single tool, they are their own test category that Adobe ships directly in the core under dev/tests/static. Unlike PHPUnit tests, which check behavior at runtime, static tests run without executing a single line of application code: they parse source files, XML configuration and layout files and evaluate them structurally. These static tests are organized into three core areas that this article focuses on: the Magento Coding Standard through phpcs, the Dependency test against undeclared module dependencies, and the Legacy test against outdated code patterns.
The boundary to two neighboring but clearly separate tools matters here. PHPStan is an external analysis tool that checks type information and data flow and is configured through its own levels and baselines, that is deliberately not the topic of this article. PHPUnit, in turn, checks actual runtime behavior of classes and modules through unit and integration tests. Static tests check neither types nor behavior, they check conventions, structural integrity and adherence to the Magento Coding Standard, regardless of whether the code even runs correctly.
The practical value of these static tests sits exactly in this niche: the Dependency test finds, for example, a module that behaves perfectly normally in local testing but breaks in production under a different module load order, because a dependency was never declared. The Magento Coding Standard, in turn, enforces conventions that matter for maintainability over years, such as avoiding direct ObjectManager calls. No other tool in the Magento toolchain covers exactly this combination of convention checking and structural checking.
2. Magento Coding Standard: the phpcs ruleset and sniff categories
The Magento Coding Standard is technically shipped through the Composer package magento/magento-coding-standard, a ruleset for PHP_CodeSniffer that Adobe itself uses for the core and for certified extensions. Running phpcs --standard=Magento2 against a custom module checks exactly the conventions applied during the Marketplace technical review. The Magento Coding Standard is notably stricter than generic PHP standards such as PSR-12, because it adds Magento specific knowledge on top.
The sniffs inside the Magento Coding Standard are grouped into functional categories: security sniffs detect unsafe constructs such as direct SQL string concatenation, performance sniffs flag inefficient patterns like database access inside loops, and PHP sniffs enforce things such as avoiding @ error silencing or var_dump in production code. A dedicated category covers pure Magento conventions, for example that constructors must never contain direct ObjectManager::getInstance() instantiation and must rely consistently on dependency injection instead.
For custom modules it is also worth combining this with the Magento2-Doc-Comments sniff for complete PHPDoc blocks, since many teams internally enforce stricter documentation requirements than the default ruleset. Installation happens through a regular Composer dev dependency, so the Magento Coding Standard grows versioned along with the project.
{
"require-dev": {
"magento/magento-coding-standard": "^32.0",
"squizlabs/php_codesniffer": "^3.9"
},
"extra": {
"magento-force": "override"
}
}
3. Setting up phpcs.xml and running the Magento Coding Standard
So that the Magento Coding Standard does not need to be specified as a command line flag on every single call, a phpcs.xml file is added at the project or module root. This file references the ruleset name Magento2, defines which file extensions to check such as php and phtml, and explicitly excludes folders such as vendor or generated directories. Without such a configuration, phpcs either checks too much, such as third party code, or too little, because template files with a .phtml extension get forgotten.
A second important piece is the installed_paths entry, which tells PHP_CodeSniffer where the Magento Coding Standard ruleset physically lives. With a Composer installation this is usually handled automatically through the package's own Composer\Installer integration, in manual setups the path must be set explicitly via phpcs --config-set installed_paths. If this step is skipped, phpcs fails with an error stating that the ruleset name Magento2 is unknown.
<!-- phpcs.xml at the project or module root -->
<?xml version="1.0"?>
<ruleset name="Mironsoft Magento Coding Standard">
<description>Runs the Magento Coding Standard against custom modules</description>
<!-- Reference the official Magento2 ruleset -->
<rule ref="Magento2"/>
<file>app/code/Mironsoft</file>
<arg name="extensions" value="php,phtml"/>
<arg name="colors"/>
<arg value="p"/>
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/var/*</exclude-pattern>
<exclude-pattern>*/generated/*</exclude-pattern>
</ruleset>
In day to day operation, a single call through the Docker wrapper is enough afterward to run the complete Magento Coding Standard against a custom module. For Mark Shust setups, a dedicated wrapper such as bin/phpcs makes sense, delegating internally to vendor/bin/phpcs inside the container.
#!/usr/bin/env bash
# Run the Magento Coding Standard against a specific module
bin/phpcs app/code/Mironsoft/SeoSuite --standard=Magento2
# Auto-fix violations that phpcbf can safely resolve
bin/phpcbf app/code/Mironsoft/SeoSuite --standard=Magento2
# Run Magento's own static test suite (Dependency, Legacy, coding style)
bin/cli vendor/bin/phpunit -c dev/tests/static/phpunit.xml.dist \
--filter "Magento\\Test\\Integrity\\Dependency"
bin/cli vendor/bin/phpunit -c dev/tests/static/phpunit.xml.dist \
--filter "Magento\\Test\\Legacy"
4. The Dependency test: catching undeclared module dependencies
The Dependency test is one of the three core building blocks of static tests and answers a question that neither the Magento Coding Standard nor PHPStan can answer: does a module use classes, blocks, layout handles or events from another module without formally declaring that dependency in module.xml? To do this, the test statically scans all PHP, XML and template code of a module, extracts every reference to a foreign class or namespace, and cross-checks it against the declared sequences.
A typical case: a custom module injects a class from Magento\ConfigurableProduct in a constructor, without listing Magento_ConfigurableProduct as a sequence dependency in its own module.xml. Locally this works without issues, because both modules are installed anyway and the load order happens to line up. But as soon as the module order changes, for example through an update or a different module combination, the referenced class may not yet be available at load time. The Dependency test raises the alarm here preventively, long before such a failure shows up in production.
In practice, the Dependency test reports every violation with the exact file path, line number and the name of the undeclared foreign class. That makes fixing it unambiguous: either the missing sequence gets added to module.xml, or the dependency is deliberately resolved through a service contract interface instead of a concrete model class, reducing coupling from the start.
5. Declaring module sequences correctly in module.xml
Every Magento 2 module's module.xml supports a sequence block alongside the plain module name, which defines after which other modules the current module must load. That is not a confirmation of a Composer dependency, it is independent information relevant to the Dependency test: Composer controls which packages are installed at all, module.xml controls the order in which they are initialized inside Magento. Both must be kept in sync, otherwise the Dependency test keeps reporting a gap even if Composer pulls in the correct version.
A missing sequence can have two visible consequences: the dependency injection configuration in di.xml gets merged in an order that does not match business expectations, or setup:upgrade scripts run before the scripts of the module they actually depend on. A correctly maintained sequence is therefore not bureaucratic formality, it is the foundation that keeps Magento's module system deterministic and reproducible.
<!-- app/code/Mironsoft/SeoSuite/etc/module.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Mironsoft_SeoSuite">
<sequence>
<!-- Declares that Magento_Catalog must load before this module -->
<module name="Magento_Catalog"/>
<!-- Declares that Magento_ConfigurableProduct must load before this module -->
<module name="Magento_ConfigurableProduct"/>
<module name="Mironsoft_Core"/>
</sequence>
</module>
</config>
A proven practice is to review the sequence after every new constructor import or every new layout reference, rather than maintaining it only once when the module is created. In practice, a regular run of the Dependency test right during development, not only shortly before release, is enough for this, since dependencies can shift with every new feature.
6. Legacy test: detecting outdated Magento patterns automatically
The Legacy test is the third building block of static tests and checks whether a module uses classes, methods or patterns that Adobe officially classifies as deprecated or as leftovers from Magento 1. Unlike the Dependency test, this is not about missing declarations, it is about deliberately forbidden or sunsetting constructs that can be removed without warning in a future major version. The Legacy test maintains internal lists of deprecated classes, methods and directory structures and compares custom code against them.
A classic example is direct use of the ObjectManager inside a constructor instead of passing the required dependency cleanly through dependency injection. This pattern still technically works, but is considered a legacy pattern because it makes testing harder, creates hidden dependencies and contradicts the entire service contract principle that Magento 2 is built on. The Legacy test reliably flags such spots long before they would be noticed manually in a code review.
<?php
declare(strict_types=1);
namespace Mironsoft\SeoSuite\Model;
use Magento\Framework\App\ObjectManager;
use Magento\Catalog\Api\ProductRepositoryInterface;
/**
* Flagged by the Legacy test: direct ObjectManager usage inside a constructor.
*/
class MetaBuilderLegacy
{
public function build(int $productId): string
{
// Legacy pattern: bypasses constructor injection entirely.
$repository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class);
$product = $repository->getById($productId);
return (string) $product->getMetaTitle();
}
}
/**
* Fixed version: dependency is injected through the constructor.
*/
class MetaBuilderFixed
{
/**
* @param ProductRepositoryInterface $productRepository Injected repository, no ObjectManager needed.
*/
public function __construct(
private readonly ProductRepositoryInterface $productRepository
) {
}
public function build(int $productId): string
{
$product = $this->productRepository->getById($productId);
return (string) $product->getMetaTitle();
}
}
Besides ObjectManager misuse, the Legacy test also checks for remaining Magento 1 calls such as Mage::, for class constants marked as deprecated, and for directory structures inherited from the old module system. For migration projects that move old custom code into a fresh Magento 2 codebase, the Legacy test is often the first hard reality check showing how much actually needs to be modernized.
7. Structure of dev/tests/static/testsuite and how to run it
All static tests technically run as a completely ordinary PHPUnit test suite located under dev/tests/static/testsuite/Magento/Test/ and configured through dev/tests/static/phpunit.xml.dist. Inside this directory there are clearly separated subfolders: Php/ contains, among others, the LiveCodeTest, which internally runs the Magento Coding Standard through phpcs, Integrity/Dependency/ contains the Dependency test, and Legacy/ contains the Legacy test along with its exception and reference lists.
What matters for custom modules is that these tests are by default controlled through whitelist or blacklist configuration files that determine which paths are even checked. A newly created module under app/code/Mironsoft is picked up automatically as long as it follows the standard namespace scheme, but specific exclusions can be added through dedicated configuration files inside the testsuite directory, for example to temporarily exempt generated or deliberately not yet migrated code.
Static tests are executed like any other PHPUnit suite, only with a dedicated configuration file as the target. Since a full run across all subfolders can take several minutes, day to day development usually filters for a single test class, for example only the Dependency test or only the Legacy test, instead of starting the entire suite every time.
8. Wiring static tests into a CI pipeline
In a CI pipeline, a two stage strategy for static tests pays off. The first stage checks only changed files on every commit or merge request with a fast phpcs run against the Magento Coding Standard, which usually takes a few seconds and provides immediate feedback. The second stage runs the complete suite under dev/tests/static/testsuite, including the Dependency test and the Legacy test across the entire codebase, either before every merge into the main branch or as a nightly run.
This split is deliberate, because the Dependency test naturally needs to look at the entire module graph to draw reliable conclusions, while a plain coding standard check works fine in isolation per file. A pipeline that only checks changed files against the Dependency test would miss dependencies that were created by changes somewhere completely different in the module.
It also matters that the pipeline actually blocks the merge on a violation, not just print a warning. Static tests only unfold their full value as a hard gate: a team that treats Magento Coding Standard violations or Dependency test failures as optional hints accumulates over months exactly the technical debt these tests are meant to prevent.
9. Comparison: Coding Standard, Dependency Test, Legacy Test and PHPStan
All three building blocks of static tests check different aspects of the same codebase and complement rather than overlap each other. The Magento Coding Standard secures conventions and style, the Dependency test secures the structural integrity of the module system, and the Legacy test secures the codebase's readiness for future Magento versions. PHPStan is mentioned here only for context: it checks type information and data flow and is a separate topic outside this article.
| Check level | Tool | Checks | Runtime |
|---|---|---|---|
| Coding Standard | phpcs + Magento2 ruleset |
Style, security and performance conventions per file | Seconds |
| Dependency Test | dev/tests/static (PHPUnit) |
Undeclared module dependencies against module.xml | Seconds to minutes |
| Legacy Test | dev/tests/static (PHPUnit) |
Deprecated classes, methods and Magento 1 patterns | Seconds to minutes |
| PHPStan (context) | PHPStan | Type safety and data flow, a separate topic | Seconds to minutes |
The practical consequence: none of these static tests replaces another. A module can fully comply with the Magento Coding Standard and still break the Dependency test because of a missing sequence, or be structurally clean and still contain legacy patterns. Only the combination of all three checks under dev/tests/static gives a complete picture of the structural and conventional quality of a Magento 2 module.
Mironsoft
Magento Coding Standard, Dependency checks and CI integration for Magento 2
Want static tests set up cleanly from the start?
We set up the Magento Coding Standard, the Dependency test and the Legacy test for your modules, from phpcs.xml through correct module.xml sequences to a hard CI gate integration.
Coding Standard
Configuring phpcs.xml and the Magento Coding Standard cleanly
Dependency Test
Fixing module.xml sequences and keeping them stable long term
CI Gate
Wiring static tests in as a hard merge gate in your pipeline
10. Summary
Static tests under dev/tests/static cover a gap in Magento 2 that neither PHPStan nor PHPUnit can close: adherence to the Magento Coding Standard, structural integrity of the module system through the Dependency test, and future readiness of the code through the Legacy test. Whoever integrates these three checks consistently into daily development, instead of running them once shortly before a release, prevents exactly the class of failure that otherwise only surfaces under a changed module order or a major upgrade.
The decisive lever is a clean phpcs.xml configuration for the Magento Coding Standard, well maintained sequences in every module.xml, and a CI gate that actually blocks violations instead of merely logging them. Combined with a two stage pipeline, a fast coding standard check on every commit and a full Dependency and Legacy check before every merge, static tests become a reliable foundation on top of which further quality assurance such as PHPStan and PHPUnit can meaningfully be built.
Static tests in Magento 2: the essentials at a glance
Magento Coding Standard
The Magento2 phpcs ruleset from magento/magento-coding-standard, checking style, security and performance.
Dependency Test
Finds undeclared module dependencies against the sequences in module.xml.
Legacy Test
Detects outdated patterns such as direct ObjectManager calls or Magento 1 leftovers.
CI operation
Fast coding standard check per commit, full suite as a hard gate before every merge.