ACL-Pruefungen, Formularvalidierung und Redirect-Logik gezielt testen
Adminhtml-Controller vereinen Zugriffskontrolle, Formularverarbeitung und Redirect-Logik an einer Stelle, weshalb sich Unit- und Integrationstests ergaenzen muessen, um sowohl die reine Logik als auch das Zusammenspiel mit dem Magento-Backend-Stack abzudecken.
Inhaltsverzeichnis
- 1. Warum Adminhtml-Controller eine besondere Testebene brauchen
- 2. Einen testbaren Adminhtml-Controller strukturieren
- 3. Die execute-Logik als Unit-Test mit gemocktem Request
- 4. ACL-Pruefungen als Integrationstest gegen die echte Berechtigungsstruktur
- 5. Formularvalidierung als eigenstaendige, isolierte Regel testen
- 6. Redirect-Ziele und Message-Typen praezise pruefen
- 7. Abgrenzung zu vollstaendigen Functional- und MFTF-Tests
- 8. Haeufige Testfallen bei Adminhtml-Controllern
- 9. Checkliste und Testebenen im Ueberblick
- 10. Zusammenfassung
- 11. FAQ
1. Warum Adminhtml-Controller eine besondere Testebene brauchen
Ein Adminhtml-Controller in Magento erbt in der Regel von Magento\Backend\App\Action und buendelt mehrere Verantwortlichkeiten: die ACL-Pruefung ueber die _isAllowed()-Methode, das Einlesen und Validieren von Formulardaten aus dem Request, das Aufrufen der eigentlichen Geschaeftslogik sowie das Setzen von Success- oder Error-Messages und die Redirect-Entscheidung. Diese Buendelung macht den Controller zu einem naturgemaess schwerer isolierbaren Baustein als eine reine Service-Klasse, weil mehrere Framework-Schichten gleichzeitig im Spiel sind und ein einzelner Test schnell zu viel auf einmal pruefen moechte.
Deshalb lohnt sich hier ein zweistufiger Test-Ansatz: Ein reiner Unit-Test prueft die Entscheidungslogik innerhalb der execute()-Methode mit gemocktem Request, Response und Session, waehrend ein Magento-Integrationstest, der von \Magento\TestFramework\TestCase\AbstractBackendController erbt, das tatsaechliche Routing, die ACL-Aufloesung ueber die echte Berechtigungsstruktur und das komplette Dispatching prueft. Beide Ebenen ergaenzen sich, statt sich zu ersetzen, und zusammen ergeben sie ein vollstaendiges Bild davon, ob ein Controller sowohl fachlich korrekt als auch sicherheitstechnisch abgesichert ist.
2. Einen testbaren Adminhtml-Controller strukturieren
Damit ein Controller ueberhaupt sinnvoll unit-testbar ist, sollte die eigentliche Verarbeitung der Formulardaten nicht direkt in execute() stattfinden, sondern an einen injizierten Service delegiert werden. Der Controller selbst bleibt fuer das Auslesen der Request-Parameter, das Aufrufen des Service und die Entscheidung ueber die Antwort zustaendig. Das ist dasselbe Prinzip wie bei Cronjobs und Queue-Consumern: duenne Eingangsschicht, ausgelagerte Fachlogik.
Zusaetzlich sollte die ACL-Ressource explizit als Konstante definiert und in _isAllowed() referenziert werden, statt den String direkt zu hardcoden. Das erleichtert es, in einem Test gezielt zu pruefen, welche Ressource fuer den Controller erforderlich ist, und verhindert Tippfehler zwischen der Controller-Klasse und der acl.xml.
<?php
declare(strict_types=1);
namespace Mironsoft\SeoSuite\Controller\Adminhtml\Redirect;
use Magento\Backend\App\Action;
use Magento\Framework\Controller\ResultFactory;
use Mironsoft\SeoSuite\Model\RedirectSaver;
/**
* Speichert eine neue Weiterleitungsregel im Adminbackend.
*/
class Save extends Action
{
public const ADMIN_RESOURCE = 'Mironsoft_SeoSuite::redirect_save';
public function __construct(
Action\Context $context,
private readonly RedirectSaver $redirectSaver
) {
parent::__construct($context);
}
/**
* Verarbeitet die Formulardaten und speichert die Weiterleitung.
*
* @return \Magento\Framework\Controller\ResultInterface
*/
public function execute()
{
$data = $this->getRequest()->getParams();
$resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
if (empty($data['from_path']) || empty($data['to_path'])) {
$this->messageManager->addErrorMessage(__('Bitte alle Pflichtfelder ausfuellen.'));
return $resultRedirect->setPath('*/*/new');
}
$this->redirectSaver->save($data['from_path'], $data['to_path']);
$this->messageManager->addSuccessMessage(__('Weiterleitung wurde gespeichert.'));
return $resultRedirect->setPath('*/*/');
}
}
3. Die execute-Logik als Unit-Test mit gemocktem Request
Fuer den reinen Unit-Test wird der Controller nicht ueber das Dispatching aufgerufen, sondern direkt instanziiert, wobei Context, Request, MessageManager und der injizierte Service gemockt werden. Der Test prueft dann, ob bei fehlenden Pflichtfeldern die Fehlermeldung gesetzt und zur richtigen Seite zurueckgeleitet wird, ohne dass der Speicher-Service ueberhaupt aufgerufen wird.
Dieser Test laeuft ohne Datenbank und ohne echtes HTTP-Routing in Millisekunden. Er deckt die eigentliche Entscheidungslogik ab: Welche Bedingungen fuehren zu welchem Redirect, welche Message wird gesetzt, wird der Service korrekt mit den erwarteten Parametern aufgerufen. Das ist die gleiche Teststrategie wie bei jeder anderen Service-Klasse, nur mit den controller-spezifischen Mocks fuer Request und MessageManager.
<?php
declare(strict_types=1);
namespace Mironsoft\SeoSuite\Test\Unit\Controller\Adminhtml\Redirect;
use Mironsoft\SeoSuite\Controller\Adminhtml\Redirect\Save;
use Mironsoft\SeoSuite\Model\RedirectSaver;
use Magento\Backend\App\Action\Context;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Message\ManagerInterface;
use Magento\Framework\Controller\Result\Redirect;
use Magento\Framework\Controller\ResultFactory;
use PHPUnit\Framework\TestCase;
class SaveTest extends TestCase
{
public function testMissingRequiredFieldsShowsErrorAndRedirectsToNew(): void
{
$request = $this->createMock(RequestInterface::class);
$request->method('getParams')->willReturn(['from_path' => '', 'to_path' => '']);
$messageManager = $this->createMock(ManagerInterface::class);
$messageManager->expects($this->once())->method('addErrorMessage');
$redirectResult = $this->createMock(Redirect::class);
$redirectResult->expects($this->once())->method('setPath')->with('*/*/new')->willReturnSelf();
$resultFactory = $this->createMock(ResultFactory::class);
$resultFactory->method('create')->willReturn($redirectResult);
$context = $this->createMock(Context::class);
$context->method('getRequest')->willReturn($request);
$context->method('getMessageManager')->willReturn($messageManager);
$context->method('getResultFactory')->willReturn($resultFactory);
$redirectSaver = $this->createMock(RedirectSaver::class);
$redirectSaver->expects($this->never())->method('save');
$controller = new Save($context, $redirectSaver);
$controller->execute();
}
}
4. ACL-Pruefungen als Integrationstest gegen die echte Berechtigungsstruktur
Waehrend der Unit-Test die Kernlogik prueft, laesst sich die tatsaechliche ACL-Durchsetzung nur sinnvoll gegen die echte Magento-Berechtigungsstruktur testen, weil dort geprueft wird, ob ein Benutzer mit einer bestimmten Rolle Zugriff auf die konfigurierte Ressource aus acl.xml erhaelt oder mit einer 403-Antwort abgewiesen wird. Dafuer bietet Magento die Basisklasse AbstractBackendController im TestFramework an, die Dispatching, Session und Autorisierung realistisch nachbildet.
Ein solcher Integrationstest simuliert einen Admin-Benutzer ohne die erforderliche Ressource und prueft, dass der Zugriff verweigert wird, sowie einen Benutzer mit der Ressource, bei dem die Aktion erfolgreich durchlaeuft. Dieser Test ist deutlich langsamer als ein Unit-Test, weil er den kompletten Magento-Bootstrap benoetigt, deckt dafuer aber genau die Stelle ab, an der ein reiner Unit-Test blind waere: das Zusammenspiel von Controller, acl.xml und Rollenverwaltung.
<?php
declare(strict_types=1);
namespace Mironsoft\SeoSuite\Test\Integration\Controller\Adminhtml\Redirect;
use Magento\TestFramework\TestCase\AbstractBackendController;
/**
* @magentoAppArea adminhtml
*/
class SaveTest extends AbstractBackendController
{
/**
* @magentoConfigFixture current_store admin/security/admin_account_sharing 0
*/
public function testAclHasAccess(): void
{
$this->dispatch('backend/mironsoft_seosuite/redirect/save');
$this->assertSessionMessages($this->equalTo([]));
}
public function testAclNoAccess(): void
{
$this->getRequest()->setParam('form_key', $this->_objectManager->get(
\Magento\Framework\Data\Form\FormKey::class
)->getFormKey());
$this->uri = 'backend/mironsoft_seosuite/redirect/save';
parent::testAclNoAccess();
}
}
5. Formularvalidierung als eigenstaendige, isolierte Regel testen
Ist die Validierungslogik komplexer als eine simple Leerstring-Pruefung, etwa eine Regel, dass from_path mit einem Slash beginnen und nicht mit der to_path identisch sein darf, lohnt es sich, diese Validierung in eine eigene Validator-Klasse auszulagern. Der Controller ruft dann lediglich validate() auf und reagiert auf das Ergebnis, waehrend die eigentliche Regelmenge in einem eigenen, feingranularen Test-Set liegt.
Diese Trennung erlaubt es, viele Validierungsvarianten ueber einen Data Provider durchzuspielen, ohne jedes Mal den kompletten Controller-Kontext aufzubauen. Aendert sich eine Validierungsregel, muss nur der Validator-Test angepasst werden, waehrend der Controller-Test unveraendert bleibt, solange die Schnittstelle zum Validator gleich bleibt.
<?php
declare(strict_types=1);
/**
* @dataProvider invalidRedirectDataProvider
*/
public function testValidatorRejectsInvalidRedirectRules(string $from, string $to, string $expectedError): void
{
$validator = new RedirectRuleValidator();
$result = $validator->validate($from, $to);
$this->assertFalse($result->isValid());
$this->assertSame($expectedError, $result->getFirstError());
}
public static function invalidRedirectDataProvider(): array
{
return [
'missing leading slash' => ['catalog/product', '/new-path', 'from_path must start with a slash'],
'identical paths' => ['/old-path', '/old-path', 'from_path and to_path must differ'],
];
}
6. Redirect-Ziele und Message-Typen praezise pruefen
Ein haeufiger Bug in Adminhtml-Controllern ist ein falsches Redirect-Ziel nach dem Speichern, etwa wenn nach dem Klick auf Speichern-und-Weiter statt zur Bearbeitungsseite versehentlich zur Listenansicht weitergeleitet wird. Solche Fehler faellt im manuellen Testen oft nicht sofort auf, weil beide Seiten valide Antworten liefern, nur eben die falsche im jeweiligen Kontext.
Ein gezielter Unit-Test, der fuer jeden Request-Parameter, etwa den Save-and-Continue-Button, das erwartete Redirect-Ziel prueft, deckt genau diese Klasse von Fehlern zuverlaessig auf. Kombiniert mit einer Ueberpruefung des Message-Typs, ob also tatsaechlich addSuccessMessage statt addErrorMessage aufgerufen wurde, ergibt sich eine praezise Spezifikation des sichtbaren Nutzerverhaltens.
<?php
declare(strict_types=1);
public function testSaveAndContinueRedirectsToEditPageWithId(): void
{
$request = $this->createMock(RequestInterface::class);
$request->method('getParams')->willReturn([
'from_path' => '/old', 'to_path' => '/new', 'back' => 'edit', 'entity_id' => '5',
]);
$redirectResult = $this->createMock(Redirect::class);
$redirectResult->expects($this->once())->method('setPath')->with('*/*/edit', ['id' => '5'])->willReturnSelf();
// ... Context-Setup analog zum vorherigen Test ...
$this->assertTrue(true);
}
7. Abgrenzung zu vollstaendigen Functional- und MFTF-Tests
PHPUnit-Tests, ob als Unit- oder Integrationstest, pruefen immer PHP-seitiges Verhalten: Wird die richtige Methode aufgerufen, wird das richtige Redirect gesetzt, stimmt die ACL-Konfiguration. Was PHPUnit bewusst nicht abdeckt, ist das tatsaechliche Rendering der Adminhtml-Oberflaeche im Browser, JavaScript-Interaktionen im UI-Component-Formular oder das visuelle Layout der Seite.
Fuer diese Aspekte ist MFTF, das Magento Functional Testing Framework, die richtige Ebene, weil es einen echten Browser steuert und den kompletten Stack von HTML ueber JavaScript bis zur Datenbank durchlaeuft. In der Praxis bewaehrt sich eine klare Aufgabenteilung: PHPUnit deckt die Logikfaelle mit vielen kleinen, schnellen Tests ab, waehrend MFTF wenige, aber kritische End-to-End-Pfade wie das erfolgreiche Anlegen eines Datensatzes ueber die Oberflaeche absichert.
8. Haeufige Testfallen bei Adminhtml-Controllern
Eine haeufige Falle ist, den ObjectManager direkt im Controller zu verwenden, um Abhaengigkeiten zu erzeugen, statt sie ueber den Konstruktor zu injizieren. Das macht den Controller im Unit-Test praktisch nicht mockbar, weil die tatsaechlich verwendete Instanz erst zur Laufzeit im echten Container aufgeloest wird. Jede Abhaengigkeit sollte deshalb explizit im Konstruktor deklariert werden, auch wenn das bei Controllern mit vielen Instanzvariablen zunaechst umstaendlicher wirkt und die Konstruktor-Signatur laenger macht als bei einer schlanken Service-Klasse.
Eine zweite haeufige Falle ist, in Tests versehentlich echte Session- oder Cookie-Objekte zu verwenden, statt sie zu mocken, was zu unvorhersehbarem Verhalten fuehrt, je nachdem, welcher Zustand von einem vorherigen Test uebrig geblieben ist. Konsequentes Mocking aller Backend-App-Context-Abhaengigkeiten verhindert solche schwer reproduzierbaren Testfehler zuverlaessig. Eine dritte, oft unterschaetzte Falle ist ein zu grosszuegig formulierter Test, der nur prueft, dass ueberhaupt irgendein Redirect zurueckgegeben wird, statt das konkrete Ziel und die konkrete Message zu verifizieren, wodurch echte Regressionen unbemerkt durchrutschen koennen.
9. Checkliste und Testebenen im Ueberblick
Wer neue Adminhtml-Controller entwickelt, sollte von Beginn an Fachlogik und Validierung in eigene Services auslagern, ACL-Konstanten explizit definieren und fuer jede execute-Verzweigung einen gezielten Unit-Test schreiben. Die ACL-Durchsetzung selbst gehoert in einen schlanken Integrationstest, waehrend das visuelle Verhalten der Oberflaeche MFTF vorbehalten bleibt.
Die folgende Tabelle ordnet die verschiedenen Testebenen rund um Adminhtml-Controller ein und zeigt, welche Ebene welchen Aspekt des Controllers abdeckt.
| Testebene | Was geprueft wird | Benoetigt Magento-Bootstrap | Typische Ausfuehrungszeit |
|---|---|---|---|
| Controller-Unit-Test | execute-Logik, Redirects, Messages mit gemocktem Request | Nein | Millisekunden |
| Validator-Unit-Test | Formularregeln unabhaengig vom Controller | Nein | Millisekunden |
| ACL-Integrationstest | Zugriffsdurchsetzung gegen echte Rollen und acl.xml | Ja | Sekunden |
| MFTF-Functional-Test | Rendering, JavaScript, kompletter Nutzerpfad im Browser | Ja (inkl. Browser) | Minuten |
Mironsoft
Testautomatisierung, Magento-Qualitätssicherung und CI-Integration
Tests, die echte Fehler finden statt nur grün zu leuchten?
Wir prüfen bestehende PHPUnit-Suiten auf Implementierungsdetail-Tests, flaky Tests und fehlende Coverage an kritischen Stellen und bauen daraus eine Teststrategie, die bei jedem Magento-Update wirklich Sicherheit gibt.
Test-Audit
Bestehende Suiten auf Mocking-Antipatterns und blinde Flecken prüfen.
Teststrategie
Unit-, Integrations- und MFTF-Tests sinnvoll für Magento-Projekte kombinieren.
CI-Integration
Schnelle, zuverlässige Testläufe in GitLab CI oder GitHub Actions einrichten.
10. Zusammenfassung
Adminhtml-Controller testen: Das Wichtigste auf einen Blick
Zwei Ebenen
Unit-Test fuer execute-Logik, Integrationstest fuer echte ACL-Durchsetzung
Duenne Controller
Fachlogik und Validierung in eigene Services und Validator-Klassen auslagern
ACL-Konstanten
ADMIN_RESOURCE explizit definieren, um Tippfehler zwischen Code und acl.xml zu vermeiden
Klare Grenze
MFTF fuer Rendering und Browser-Interaktion, PHPUnit fuer PHP-seitige Logik