Closed calvinalkan closed 3 weeks ago
Apart from dataProviders, this seems to be a bug in general, since a couple of lines after we try so commit some transaction which should never have any changes to commit because we just connected.
public static function _setUpBeforeClass()
{
static::setupForSeparateProcessBeforeClass();
global $wpdb;
$wpdb->suppress_errors = false;
$wpdb->show_errors = true;
$wpdb->db_connect();
ini_set('display_errors', 1);
$c = self::get_called_class();
if (!method_exists($c, 'wpSetUpBeforeClass')) {
// THIS IS A BRAND NEW CONNECTION, nothing to be commited?
self::commit_transaction();
return;
}
call_user_func(array($c, 'wpSetUpBeforeClass'), self::factory());
// THIS IS A BRAND NEW CONNECTION, nothing to be commited?
self::commit_transaction();
}
And
Hi @calvinalkan,
This is not a bug, but something that works like that in the Core PHPUnit suite as well, and has always been working like this.
Your observations are correct: the WPTestCase
class will start a transaction at the start of tests methods, not during set up of the test case or before data providers.
Any database change made to before the start of a test method will not happen in a transaction, but will become part of the database fixture.
The calls to commit transactions are there for back-compatibility with some implementations extending the set up methods, in your specific application, they might not be committing anything to the database, but it's not a huge overhead.
Data providers: PHPUnit will run and collect the output of all the data-providers before any test method runs, this is how PHPUnit works to this day.
setUp
methods run and WPTestCase::setUp
starts the db transactiontearDown
method rolls back the database changes - the db state goes back to "fixture"One approach is the Core suite one where tests are written to be very conscious of a mixed db state. E.g. if a test creates a post
and other data providers (thus part of the fixture) created post
s before, one will not run this:
self::factory()->post->create();
$this->assertCount(1, get_posts());
But this:
$id = self::factory()->post->create();
$this->assertInstanceOf(WP_Post::class, get_post($id));
Or some other form of very discerning and conscious assertion.
What I, personally, prefer is:
setUp
method, after the transaction started, i.e. after the parent::setUp()
callWhile the first two are obvious, the third one might not.
Say I want to snapshot test the HTML of a component when there are no posts, one post, three posts; the following code will not work as I might expect:
<?php
use lucatume\WPBrowser\TestCase\WPTestCase;
class Component_Test extends WPTestCase
{
public function postsProvider(): array
{
return [
'no posts' => [[]],
'one post' => [
[
self::factory()->post->create()
]
],
'three posts' => [
[
self::factory()->post->create(),
self::factory()->post->create(),
self::factory()->post->create(),
]
],
];
}
/**
* @dataProvider postsProvider
*/
public function test_fixture(array $ids): void
{
$this->assertCount(count($ids), get_posts());
}
}
All tests will fail as the fixture contains 4 posts.
I might go the route of creating a test for each number of post, e.g. test_with_no_posts
, test_with_one_post
and so one, but I like using data providers.
So I just return Closures that will run in the test method, hence a transaction:
<?php
use lucatume\WPBrowser\TestCase\WPTestCase;
class Component_Test extends WPTestCase
{
public function postsProvider(): array
{
return [
'no posts' => [fn() => []],
'one post' => [
fn() => [
self::factory()->post->create()
]
],
'three posts' => [
fn() => [
self::factory()->post->create(),
self::factory()->post->create(),
self::factory()->post->create(),
]
],
];
}
/**
* @dataProvider postsProvider
*/
public function test_fixture(\Closure $fixture): void
{
$ids = $fixture();
$this->assertCount(count($ids), get_posts());
}
}
Hope this helps you.
Thanks, Luca. That all make/made sense and is how it expect it to work anyways.
Any database change made to before the start of a test method will not happen in a transaction, but will become part of the database fixture.
Correct, this is the correct/expected behavior. I don't know why we need two distinct DB connections for that, though.
It would work the same with one DB connection.
Anything before setUp() is a fixture, anything between setUp() and tearDown() is wrapped in a transaction.
I've fixed this in my BaseIntegrationTestCase and it works like so:
I don't ever use dataproviders to create data, just to create classes (sometimes with closures).
The issue that I had is quite clear with the following example.
<?php
use Codeception\TestCase\WPTestCase;
interface CanCountUsers {
public function countItems() :int;
}
class RepositoryDatabaseWithWPDB implements CanCountUsers {
private wpdb $db;
public function __construct(wpdb $db) {
$this->db = $db;
}
public function countItems() :int
{
return (int) $this->db->get_var('select count(*) from wp_users');
}
}
class SomeTestCase extends WPTestCase {
/**
* @test
*/
public function test_that_count_works(CanCountUsers $test_subject) :void
{
$this->assertSame(0, $test_subject->countItems());
wp_insert_user(['user_login' => 'foo', 'user_pass' => 'bar']);
// THIS WILL ALWAYS FAIL. wp_insert_user was called inside a transaction.
// BUT IN A DIFFERENT DB CONNECTION.
$this->assertSame(1, $test_subject->countItems());
wp_insert_user(['user_login' => 'foo', 'user_pass' => 'bar']);
$this->assertSame(2, $test_subject->countItems());
}
public function implementation () :Generator {
yield 'db' => [new RepositoryDatabaseWithWPDB($GLOBALS['wpdb'])];
// more implementations here.
}
}
I guess it would have worked if I return a closure in the Generator.
But I'd have never thought that the wpdb instance would be different/re-created.
What version of wp-browser are you using? Or: what version is your fork based on?
The database does not connect twice or reconnect by design or intention.
Based on 3.X
But I checked that this is still present in the latest 3.5.
Thanks @calvinalkan for the detailed report: I've gone through it a second time and understood where I had understood the issue.
My apologies for dev-splaining: thank you for being graceful in receiving it.
I've applied your suggested change and released it on both main branch (version 4) and v3.5
branch.
No worries.
TBH, I'd have just thrown an exception if the connection went away, because the bug's still there.
Now the error case is just hidden, but the success case works.
I'd have just thrown an exception if the connection went away, because the bug's still there.
So, change to this:
- if ( ! $wpdb->check_connection() ) {
+ if ( ! $wpdb->check_connection(false) ) {
$wpdb->db_connect();
}
Yours is a fair point. I'm reopening the issue and following up next.
I'd do this:
- if ( ! $wpdb->check_connection() ) {
+ if ( ! $wpdb->check_connection(false) ) {
+ $this->failTest('The database connection went away. Tests might give false positives/negatives.');
}
The wpdb::check_connection
method will re-connect to the database if it can; the return value will then be true
.
That check will pass, no exception/error will be thrown, and the tests will move on.
I've experimented changing the line to
if ( ! $wpdb->check_connection(false) ) {
self::fail('The database connection went away. Tests might give false positives/negatives.');
}
and running this test:
<?php
use lucatume\WPBrowser\TestCase\WPTestCase;
class BreakingTest extends WPTestCase
{
public static function setUpBeforeClass():void
{
global $wpdb;
$wpdb->close();
parent::set_up_before_class();
}
public function test_something():void{
$this->assertTrue(true);
}
}
It will pass without issues.
The thing to check would is the dbh
property of the global wpdb
object, I can do that with reflection since it's protected
.
Changing the code to this:
if ( empty(Property::readPrivate($wpdb, 'dbh')) ) {
self::fail('The database connection went away. A `setUpBeforeClassMethod` likely closed the connection.');
}
throws an error since the connection was closed. I think this might be a good solution to make sure the connection is never dropped during set up of the tests.
What I did is query for the connection ID in before lass, and setup to ensure it does not change during one test run.
That way, you don't have to mess with reflection.
https://dev.mysql.com/doc/refman/8.0/en/information-functions.html#function_connection-id
I've settled for checking the connection id as suggested (thank you for the suggestion) and adding a new, true
by default, beStrictAboutWpdbConnectionId
configuration parameter to the WPLoader
module configuration.
I've settled for checking the connection id as suggested (thank you for the suggestion) and adding a new,
true
by default,beStrictAboutWpdbConnectionId
configuration parameter to theWPLoader
module configuration.
That sounds reasonable. Cheers.
We're still using v3 (a fork actually) but the issue described below is also present in the latest commit on v3.
https://github.com/lucatume/wp-browser/blob/334cfebd01afe4db317b6f4c99c90fdcfd2d0d18/src/Codeception/TestCase/WPTestCase.php#L92
WPLoader
connects to the database after installing WordPress (it calls wp-settings.php), then in thesetUpBeforeClass
method ofWPTestCase
we connect to the database again (see link above).I'm not quite sure why it's done that way, but apart from the overhead of connecting twice, it never seemed to cause issues.
However, today I faced a situation where the transactions of the
WPTestCase
were entirely being ignored and data was committed straight away.It took me forever to track this down, but the issue is because
dataProviders
are resolved beforesetUpBeforeClass
is called, and I had a class that tookwpdb
as a constructor dependency to do some lower level operations on the actualdbh
(mysqli connection).So the
dataProvider
getsmysqliA
(from wp-settings.php), and theWPTestCase
manages transactions formysqliB
(from the second connection insetupBeforeClass
).This issue would probably not have surfaced if I did not operate on the
dbh
directly (sincewpdb
is not overwritten globally) but it still seems unneeded to connect twice to the DB and there are probably other edge cases where this might cause issues.Basically, if any kind of setup work is performed anywhere between the
bootstrap.php
wp-settings call and thesetupBeforeClass
this issue will surface.Proposed Solution:
I'm assuming there is a good reason for trying to connect again to the db in WPTestCase, but it should probably be wrapped with an if in case somebody already used WPLoader.