How tests improve your architecture
Daniel Speicher
17 March 2022
Components | Sources | Tools |
---|---|---|
Drupal Environment | PHP Classes PHP Traits Schemas |
PHP Code Analysis PHP Unit Drupal Kernel Test |
Website | html css JavaScript |
Drupal Functional Test Functional-JS Test Backstop |
Use Cases | UML Diagrams User-Stories Feature Requests |
Behat |
Feature: Blog entry
In order to write a blog
As an editor
I need to be able to put text into a multiline field
Rules:
- Minimum lines of text is 10
- Maximum lines of text is 200
Scenario: Writing a blog under 10 lines
Given there is a text with 9 lines
When I submit the blog
Then I should get an error
And the blog must not be submitted
Example Test Code:
use Behat\Behat\Tester\Exception\PendingException;
use Behat\Behat\Context\SnippetAcceptingContext;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
class FeatureContext implements SnippetAcceptingContext {
/**
* @Given there is a text with :arg1 lines
*/
public function thereIsATextWithLines($arg1) {
// ...
}
//...
}
Behat configuration file behat.yml
:
default:
extensions:
Drupal\DrupalExtension:
...
autoload:
'': "%paths.base%/bootstrap/context/"
suites:
default:
contexts:
- Drupal\DrupalExtension\Context\DrupalContext
- Drupal\DrupalExtension\Context\MinkContext
...
Drupal Behat test for anonymous users:
Feature: Anonymous User Tests
In order to protect data of the company and its customers
As an anonymous user
I want to make sure that no data is available to anonymous visitors
Background:
Given I am not logged in
Scenario: Check general availability of the site and the anonymous user site
When I am on "user"
Then I should see the text "Enter the password that accompanies your username."
And I should not see the text "Access denied"
DrupalContext
:
/**
* @Given I am an anonymous user
* @Given I am not logged in
* @Then I log out
*/
public function assertAnonymousUser()
{
// Verify the user is logged out.
$this->logout(true);
}
MinkContext
:
/**
* @Then I (should )see the text :text
*/
public function assertTextVisible($text)
{
// Use the Mink Extension step definition.
$this->assertPageContainsText($text);
}
Feature: Anonymous User Tests
In order to protect data of the company and its customers
As an anonymous user
I want to make sure that no data is available to anonymous visitors
Background: # features/basic/anonymous.feature:6
Given I am not logged in # Drupal\DrupalExtension\Context\DrupalContext::assertAnonymousUser()
Scenario: Check general availability of the site and the anonymous user site # features/basic/anonymous.feature:9
When I am on "user" # Drupal\DrupalExtension\Context\MinkContext::visit()
Then I should see the text "Enter the password that accompanies your username." # Drupal\DrupalExtension\Context\MinkContext::assertTextVisible()
And I should not see the text "Access denied" # Drupal\DrupalExtension\Context\MinkContext::assertNotTextVisible()
1 scenario (1 passed)
4 steps (4 passed)
0m0.19s (16.55Mb)
Execute: vendor/bin/phploc "$@"
Size
Lines of Code (LOC) 365
Comment Lines of Code (CLOC) 104 (28.49%)
Non-Comment Lines of Code (NCLOC) 261 (71.51%)
Logical Lines of Code (LLOC) 80 (21.92%)
Classes 80 (100.00%)
Average Class Length 80
Minimum Class Length 80
Maximum Class Length 80
Average Method Length 5
Minimum Method Length 1
Maximum Method Length 24
Average Methods Per Class 14
Minimum Methods Per Class 14
Maximum Methods Per Class 14
...
Execute: vendor/bin/phpcpd "$@"
Found 1 clones with 65 duplicated lines in 1 files:
- /var/www/html/web/modules/custom/mobimo/src/EventSubscriber/UserCreationSubscriber.php:215-280 (65 lines)
/var/www/html/web/modules/custom/mobimo/src/EventSubscriber/UserCreationSubscriber.php:285-350
9.04% duplicated lines out of 719 total lines of code.
Average size of duplication is 65 lines, largest clone has 65 of lines
Time: 00:00.006, Memory: 4.00 MB
Execute: vendor/bin/phpcs -standard=DrupalPractice
FILE: /var/www/html/web/modules/custom/mobimo/src/EventSubscriber/UserCreationSubscriber.php
-------------------------------------------------------------------------------------------------
FOUND 0 ERRORS AND 1 WARNING AFFECTING 1 LINE
-------------------------------------------------------------------------------------------------
284 | WARNING | User::load calls should be avoided in classes, use dependency injection instead
-------------------------------------------------------------------------------------------------
Time: 3.56 secs; Memory: 100.01MB
Execute: vendor/bin/phpmd <path> json design
{
"files": [
{
"file": "\/var\/www\/html\/web\/modules\/custom\/mobimo\/src\/EventSubscriber\/UserCreationSubscriber.php",
"violations": [
{
"beginLine": 25,
"endLine": 368,
"package": "Drupal\\mobimo\\EventSubscriber",
"function": null,
"class": "UserCreationSubscriber",
"method": null,
"description": "The class UserCreationSubscriber has a coupling between objects value of 15. Consider to reduce the number of dependencies under 13.",
"rule": "CouplingBetweenObjects",
"ruleSet": "Design Rules",
"externalInfoUrl": "https:\/\/phpmd.org\/rules\/design.html#couplingbetweenobjects",
"priority": 2
}
]
}
]
}
UnitTestBase
in Drupal\Tests\
/**
* Tests to token browser service.
*
* @group eca
*/
class TokenBrowserServiceTest extends UnitTestCase {
public function testTokenModuleNotInstalled(): void {
$tokenBrowserService = new TokenBrowserService();
$this->assertEquals([], $tokenBrowserService->getTokenBrowserMarkup());
}
}
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entityTypeManager = $this->createMock(EntityTypeManagerInterface::class);
}
protected function setUp(): void {
parent::setUp();
$currentTimestamp = 1515510000; //2018/01/09 16:00:00
$this->time = $this->createMock(TimeInterface::class);
$this->time->expects($this->once())->method('getRequestTime')
->willReturn($currentTimestamp);
}
/**
* Tests the get and set methods.
*
* @return void
*/
public function testGetterAndSetter(): void {
$currentTimestamp = 1515510000; //2018/01/09 16:00:00
$this->time->expects($this->once())->method('getRequestTime')
->willReturn($currentTimestamp);
$this->keyValueFactory->expects($this->once())->method('get')
->with('eca')->willReturn($this->keyValueStore);
$ecaState = new EcaState($this->keyValueFactory, $this->time);
$ecaState->setTimestamp(self::TEST_KEY);
$this->assertEquals($currentTimestamp, $ecaState->getTimestamp(self::TEST_KEY));
}
\Drupal::currentUser()
extractClass
, etc. neededKernelTestBase
in Drupal\Tests\
protected static $modules = [
'user',
];
public function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installConfig(static::$modules);
User::create(['uid' => 1])->save();
}
public function test(): void {
$titleModified = $title . '_test';
$node = Node::create([
'type' => 'article',
'tnid' => 0,
'uid' => 1,
'title' => $title,
]);
$node->save();
$this->assertEquals($node->label(), $titleModified);
}
BrowserTestBase
in Drupal\Tests\
public function test(): void {
$this->adminUser = $this->drupalCreateUser([
'access toolbar',
'access administration pages',
]);
$this->drupalLogin($this->adminUser);
$this->drupalGet('/admin');
$this->assertMenuHasHref('/admin/structure/');
}
WebDriverTestBase
in Drupal\Tests\
$page = $this->getSession()->getPage();
$page->selectFieldOption('plugin', 'csv');
$this->assertSession()->assertWaitOnAjaxRequest();
$assert->elementExists('css', 'input[name="files[plugin_configuration_csv_file]"]');
$page->fillField('label', 'Test CSV Importer');
$this->assertJsCondition('jQuery(".machine-name-value").html() == "test_csv_importer"');
"viewports": [
{
"label": "phone",
"width": 320,
"height": 480,
},
...
]
"scenarios": [
{
"label": "test_screen",
"url": "https://testurl.com",
"referenceUrl": "https://produrl.com",
"hideSelectors": ['chat'],
"selectors": [],
"misMatchThreshold": 0.1,
...
},
...
]
Test Tool | Status |
---|---|
Behat | In Use |
PHP Static Code Analysis | Partly in Use |
PHP Unit Tests | in Use |
PHP Kernel Tests | in Use |
PHP Functional Tests | in Use |
PHP Functional JavaScript Tests | X |
Backstop | planed |