Problem/Motivation

WebAssertTest is slow. It even declares itself as such. It has 25 individual tests and each of them enable the test_page_test module, then perform one or more drupalGet calls. On my localhost this takes almost 4 minutes! After converting to a Unit test this takes 89 ms!

Steps to reproduce

phpunit --configuration /path/to/core/phpunit.xml.dist /path/to/core/tests/Drupal/FunctionalTests/WebAssertTest.php

Proposed resolution

Create a lightweight mock client that extends \Symfony\Component\BrowserKit\AbstractBrowser and allows tests to set expected responses. Convert the test to a Unit test.

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

Issue fork drupal-3454092

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mstrelan created an issue. See original summary.

mstrelan’s picture

Status: Active » Needs review
mstrelan’s picture

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new90 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

mstrelan’s picture

Status: Needs work » Needs review
nicxvan’s picture

This is a massive improvement for this test!

Looks good to me generally, but there was a comment from you in slack in response to a question that I think should be recorded here and addressed.

https://drupal.slack.com/archives/C1BMUQ9U6/p1718924045498369
mstrelan
3 hours ago
so the thing is that this test is not testing that the escaping thing works correctly, its testing that the assert methods work correctly

So in reviewing this I realize this is kind a meta-test. It's a test testing a test class. Normally when there is a bugfix or feature change you need a test only job to prove there is actual coverage. That won't quite work here cause the only thing changed was the test itself so test only would be the same change!

I wonder if we need another branch with these changes but the WebAssert class intentionally broke to prove this version still has the same coverage with the same rules as test only, it should fail.

That being said it looks good to me and consistent, I just don't know if we need the equivalent of the test only job in order to verify.

mstrelan’s picture

I wonder if we need another branch with these changes but the WebAssert class intentionally broke to prove this version still has the same coverage with the same rules as test only, it should fail.

I see where you're coming from, I think the test itself already covers this by expecting exceptions. Each test asserts both the positive and the negative.

nicxvan’s picture

Well I decided to do an audit since I was curious and there may be a couple of gaps for a followup.
I wanted to learn more about WebAssert anyway.

I cloned your branch and ran the WebAssertTest. All green!
In Web Assert I then would break the function by commenting out the assert or otherwise breaking the logic.

I have confirmed coverage for:

  • responseHeaderExists
  • responseHeaderDoesNotExist
  • pageTextMatchesCount
  • buttonNotExists
  • linkExistsExact
  • linkNotExistsExact
  • linkByHrefExists
  • linkByHrefExistsExact
  • linkByHrefNotExists
  • linkByHrefNotExistsExact
  • pageTextContainsOnce
  • pageContainsNoDuplicateId
  • addressNotEquals
  • elementTextEquals
  • buttonExists

WebAssertTest claims coverage, but I cannot trigger failures

  • linkExists
  • assertEscaped
  • responseContains

WebAssertTest does not claim coverage, but probably should cover these in a followup

  • optionExists
  • buildXPathQuery
  • optionNotExists
  • titleEquals
  • assert
  • fieldDisabled
  • fieldEnabled
  • hiddenFieldExists
  • hiddenFieldNotExists
  • hiddenFieldValueEquals
  • hiddenFieldValueNotEquals
  • statusMessageExists
  • statusMessageNotExists
  • statusMessageContains
  • statusMessageNotContains
  • buildStatusMessageSelector
  • responseHeaderEquals
  • pageTextContains
  • fieldValueEquals
  • selectExists
nicxvan’s picture

Just to expand linkExists is covered by testPipeCharInLocator:

public function testPipeCharInLocator(): void {
    $this->visit('/test-pipe-char', '<a href="http://example.com">foo|bar|baz</a>');
    $this->assertSession()->linkExists('foo|bar|baz');
    $this->addToAssertionCount(1);
  }

Removing the assertion should fail the test if I understand it correctly but does not.

public function linkExists($label, $index = 0, $message = '') {
    $message = ($message ? $message : strtr('Link with label %label not found.', ['%label' => $label]));
    $links = $this->session->getPage()->findAll('named', ['link', $label]);
    $this->assert(!empty($links[$index]), $message);
  }
mstrelan’s picture

I think we need to address everywhere that we're calling addToAssertionCount. Basically \Drupal\Tests\WebAssert::assert doesn't actually assert anything, but throws an exception if the condition failed. Not sure why it passed before when this was a functional test, probably there is some other assertion involved in setup or drupalGet.

EDIT: What I mean is that WebAssert::assert doesn't increment the assert counter, so we get errors that our test performed no assertions. This is how phpunit does it in Assert::assertThat - https://github.com/sebastianbergmann/phpunit/blob/10.5/src/Framework/Ass...

nicxvan’s picture

One final comment for now, I didn't check if I could get failures for the three on HEAD with the same technique, so the tests could have been broken already and not affected by this.

  • linkExists
  • assertEscaped
  • responseContains

I would think that would make fixing those a follow up too, but I can't check that right now.

Edit: crossposted with @mstrelan, I think he likely pinpointed the reason in the comment above.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new90 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

mstrelan’s picture

Status: Needs work » Needs review
catch’s picture

Issue tags: +Test suite performance
catch’s picture

This looks great and similar-ish to #3390193: Add a drupalGet() method to KernelTestBase although that is for kernel tests and more generic.

I left one (non rhetorical) question on the MR. Do we actually need different URLs for the visit calls at all, or could those just use '/' everywhere?

mstrelan’s picture

Thanks @catch. Re #16

Do we actually need different URLs for the visit calls at all, or could those just use '/' everywhere?

At least testAddressEquals needs the path set, the others could just use / or even NULL or '' probably.

nicxvan’s picture

@catch, also if you agree that the three tests mentioned in 12 can be handled in a follow up we can create those, otherwise they should be address here.

catch’s picture

@nicxvan I have to admit I'm not entirely sure what's going on with those three tests - is it that they're false negative failures in HEAD due to assert count being incremented in functional tests unrelated to the assertions we're trying to test?

If that's the case and we're not making it worse, then a follow-up seems fine.

catch’s picture

Just realised a huge advantage here is not only the performance improvement (that would have been more than enough for me ;)) but the fact that if we break one of these, we'll know it's because we actually broke it instead of some side effect.

mstrelan’s picture

#19 is spot on. Something Else (TM) is incrementing the assert count in functional tests, but I didn't dig enough to figure out what it was. Might be something in the mink setup or similar. We could probably replicate that behaviour by asserting something in the visit function, but that's a bit sneaky.

catch’s picture

OK yeah that seems like a fatal flaw in the existing test and not a problem for this issue to solve.

nicxvan’s picture

Yeah that was the issue, they don't actually test those methods.

I created #3462676: WebAssert Tests do not actually test all methods

I think the only remaining question is whether to remove the controllers you mentioned @mstrelan

catch’s picture

catch’s picture

mstrelan’s picture

Found out why those assert methods that make no assertions are passing in BrowserTestBase tests:

In BrowserTestBase::setUp:

// Ensure that the test is not marked as risky because of no assertions. In
// PHPUnit 6 tests that only make assertions using $this->assertSession()
// can be marked as risky.
$this->addToAssertionCount(1);
nicxvan’s picture

I've created another follow up #3465507: Deprecate and / or remove unused Controllers and routes from WebAssert tests I'll do another thorough review, but after discussing in slack I think the controller deprecation is better served in a follow up since there is such an extreme improvement here.

nicxvan’s picture

Status: Needs review » Reviewed & tested by the community

Follow ups have been created for the two issues called out. Code changes look good and tests are green. RTBC

catch’s picture

Status: Reviewed & tested by the community » Needs work

One remaining thing here - I think we should pass an empty string or NULL to ::visit() instead of the path names, so it's clearer that there's no path involved. Otherwise this looks great and agreed with tackling the possibly unnecessary routes/controllers in a follow-up, that's going to need its own round of investigation.

mstrelan’s picture

Status: Needs work » Needs review

Updated the docs for ::visit so it's a little more obvious and replaced all the dummy URIs with empty strings. We do still have to call $this->session->visit($uri) which takes a string, so went with empty string over NULL. Would be good to used named arguments for calls to ::visit but I think we try to avoid that. We could split ::visit in to a separate method for each of the params, since I don't think more than one is ever set now, but not sure it's worth it.

nicxvan’s picture

Status: Needs review » Reviewed & tested by the community

I checked the latest changes and I think all of @catch's requests have been resolved.

  • catch committed 136e3dcb on 10.3.x
    Issue #3454092 by mstrelan, nicxvan, catch: Convert WebAssertTest to a...

  • catch committed 678119bb on 10.4.x
    Issue #3454092 by mstrelan, nicxvan, catch: Convert WebAssertTest to a...

  • catch committed f7cdcd2f on 11.0.x
    Issue #3454092 by mstrelan, nicxvan, catch: Convert WebAssertTest to a...

  • catch committed 82e71598 on 11.x
    Issue #3454092 by mstrelan, nicxvan, catch: Convert WebAssertTest to a...
catch’s picture

Version: 11.x-dev » 10.3.x-dev
Status: Reviewed & tested by the community » Fixed

Yes looks great.

Committed/pushed to 11.x and backported through to 10.3.x to keep the test coverage in sync, thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.