Problem/Motivation

Currently, migrate_drupal unit tests which extend MigrateSqlSourceTestCase -- which is pretty much all of them -- rely on the fake DB driver in core. Unfortunately, this driver is a) extremely limited in what it can support, which limits what we can test, and b) doesn't always behave just like a real database would, which is extremely bad since that will affect the results of the tests and can lead to unreliable results. To me, these are critical problems that render the fake driver unsuitable for unit testing.

Proposed Resolution

SQLite to the rescue! It is possible for SQLite to create an in-memory database which ceases to exist when the connection is closed. In-memory databases are totally compatible with PDO and Drupal's database layer. They obviously require the PHP SQLite extension, but that's acceptable for testing purposes. In fact, parts of Drupal Module Upgrader's test suite use this exact approach. I propose that migrate_drupal's unit tests ditch the fake database driver in favor of the core SQLite driver and an in-memory database. This will greatly increase the robustness and accuracy of the unit tests.

Remaining Tasks

Review and commit the patch.

API Changes

None.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

phenaproxima’s picture

Status: Active » Needs review
FileSize
33.57 KB

Status: Needs review » Needs work

The last submitted patch, 1: 2499835-1.patch, failed testing.

chx’s picture

Great work! I recommend changing the comment in UserTest to getDatabase() will not create empty tables, so we populate with some data even if irrelevant or somesuch, the current language does not mesh with the intended level of professionalism of core.

For the sake of history, Fake was written in 2013 predating the sqlite option #1808220: Remove run-tests.sh dependency on existing/installed parent site so we couldn't rely on sqlite because the bots didn't have it yet. Probably it should now be removed from core as broken. Oh well, at least we tried.

chx’s picture

Also, reset($rows) is twice in getDatabase.

Perhaps not in this issue but this could and should be extended to replace Fake with this in-memory sqlite version where the relevant insert/merge classes create their tables (while sqlite requires a schema it's typeless; the column specification is merely a type affinity but the values themselves can provide another affinity) and query return peacefully if a table is not found.

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
56.13 KB
25.2 KB

Refactored MigrateSqlIdMapTest to work with SQLite.

chx’s picture

+  protected function saveMap(array $map) {

needs doxygen.

protected function getIdMap($database_contents = array(), $connection_options = array(), $prefix = '') {

Do we need those arguments now? Seems not.

+      $fields = array_map(function() { return ['type' => 'text']; }, $map);
+      $schema->createTable($table, ['fields' => $fields]);

This repeats. Perhaps a function in the base then?

$row['options'] = 'a:0:{}';

MMmmmm I guess. Perhaps serialize([]) ? You even changed a serialize(array()) to this, why...? Yes, I see it's in HEAD too but ... let's standardize on serialize([[]) please.

+    // $result needs to be countable, or it will fail certain assertions.

Perhaps the "return value" needs to be countable? I think that's what you meant.

@dataProvider lookupSourceIDMappingData

Core uses the word 'provider' in the name of the provider method in 410 out of the 435 provider methods. I'd say we should too.

cilefen’s picture

+++ b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php
@@ -29,11 +30,15 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
+    'source_id_property' => array(
+      'type' => 'string',
+    ),

@phenaproxima I suggest using the new array [] syntax if you are reformatting or adding arrays.

phenaproxima’s picture

FileSize
57.73 KB
8.82 KB

All fixed!

cilefen’s picture

+++ b/core/modules/migrate_drupal/tests/src/Unit/source/d6/NodeRevisionByNodeTypeTest.php
@@ -40,9 +40,97 @@ class NodeRevisionByNodeTypeTest extends MigrateSqlSourceTestCase {
 
+  protected $databaseContents = array(
+    'node' => array(
+      array(
+        'nid' => 1,
+        'type' => 'page',
+        'language' => 'en',

There are a few more array()s here and in some later files in the patch.

phenaproxima’s picture

FileSize
77.94 KB
35.19 KB

Switched to short array syntax in all the tests I modified.

chx’s picture

Status: Needs review » Reviewed & tested by the community

I think this is good to go then.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

So no we have no usages of Fake in Core? I think this patch should also remove it.

Migrate tests that dependon SQLite should fail with a requirements error if it is not installed. See TestBase::checkRequirements()

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
122.18 KB
42.11 KB

@alexpott, none of the tests depend on SQLite specifically. What they depend on is having a proper database to play with. The reason this patch switches from Fake to SQLIte is because Fake itself is broken, and SQLite is a convenient, functional replacement with the crucial advantages of Fake (operating only in memory) and none of the problems. Setting an SQLite requirement on every test doesn't really make a lot of sense to me, since virtually all of them depend on the presence of a database, and they could be run against MySQL or another similar anyway (except that's not feasible for unit testing).

However, after grepping core for "FakeConnection" and finding no additional mentions, I'm absolutely delighted to remove it. :) Fix attached.

phenaproxima’s picture

Title: migrate_drupal's unit tests should use SQLite » Remove broken Fake DB driver

Renaming the issue.

Status: Needs review » Needs work

The last submitted patch, 13: 2499835-13.patch, failed testing.

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
122.92 KB
608 bytes

Let's greenify that.

chx’s picture

Status: Needs review » Reviewed & tested by the community

Let's do this, then.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/migrate/tests/src/Unit/MigrateTestCase.php
@@ -7,15 +7,16 @@
+use Drupal\Core\Database\Driver\sqlite\Connection;

@@ -54,21 +55,54 @@ protected function getMigration() {
+    $connection_options['database'] = ':memory:';

This creates a dependency on sqlite - if environment does not have sqlite this will fail hard. We should add a set up method to check that the PHP sqlite3 extension is installed. And previous these tests had no dependency on any database.

There is a ton of changing array syntax from array() to [] in the patch which is a shame since its large enough as it is.

phenaproxima’s picture

And previous these tests had no dependency on any database.

Not true. They depend on a database, which the Fake driver was emulating. To be sure, they were not depending on any specific database, and they still don't. (That's the whole purpose of Drupal's database layer, right? :) getDatabase() makes the executive decision to supply them with an in-memory SQLite database. So adding a check for the SQLite extension is reasonable, but it shouldn't go in the setUp() method since that will affect every test, including the ones which don't need a database.

There is a ton of changing array syntax from array() to [] in the patch which is a shame since its large enough as it is.

@cliefen requested this in #7 and #9.

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
123.06 KB
1.28 KB

Added a check for the pdo_sqlite extension to the getDatabase() method.

dawehner’s picture

Its odd, I mean we are testing something related to a database. Aren't we doing that in million other tests as well, so yeah I agree with @alexpott that we should not need something special, but I see you are using a pure unit test here.

phenaproxima’s picture

I see you are using a pure unit test here.

Well, they're technically integration tests, since they're now interacting with a non-mocked database. Once KTBNG lands, we could change them to use prefixed tables in MySQL or Postgres instead, if we want to. The most important thing here, IMO, is to stop using the Fake driver and kick it out of core before anyone else decides to use it, because it is both redundant and broken.

chx’s picture

FileSize
123.79 KB
1.91 KB

These two points are definitely just my opinion but these are the reasons these tests are as they are.

It is important to see the migrate Drupal 6 sources being completely independent of Drupal 8. This makes KernelTestBase less usable. There's too much of D8 there. That alter in the select is quite wrong because it introduces a potencial circular dependency: load module -- try to load from cache -- select tries to alter -- modules are not loaded... kaboom! it doesn't because the cache doesn't add alter tags ATM. Because of this I have reverted the mock module handler from the patch and made Select Drupal independent again. DBTNG being Drupal independent was an important design consideration...

On the other hand I have serious problems with many unit tests. See, a typical query method does $this->select('files', 'f')->fields(... and that's it. Mostly. If you write $select_mock->expects()->method('fields)->willReturnSelf() or somesuch you could just as well parse the file and whether the method equals to a pre-stored copy of it character by character, it would roughly be equivalent. You are copying the code with a slightly altered syntax. Utterly useless. Whether the query actually selects anything useful you can only guess since the test is written as a copy-paste of the method tested. I'd rather create a database, put data in it, and see what I retrieve makes sense. If you want to be fancy: I do not see the point in unit testing a function with a cyclomatic complexity of 1. I know this is a personal thing but ... this is what you got when you accepted me as the migrate architect. You could, of course, rewrite this as "pure" unit tests. It's not something I would do but since I am not coding this, feel free. Same for using KTB.

Status: Needs review » Needs work

The last submitted patch, 23: 2499835_23.patch, failed testing.

chx queued 23: 2499835_23.patch for re-testing.

chx’s picture

Status: Needs work » Needs review

Weird bot fail.

dawehner’s picture

I totally agree that these tests don't make sense if they mock the database connection ...

benjy’s picture

There is a ton of changing array syntax from array() to [] in the patch which is a shame since its large enough as it is.

I agree with this, pointless noise in the patch which in turn made it much harder to review.

  1. +++ b/core/lib/Drupal/Core/Database/Query/Select.php
    @@ -496,12 +496,23 @@ public function preExecute(SelectInterface $query = NULL) {
    -    if (isset($this->alterTags)) {
    +    if (!empty($this->alterTags)) {
    

    What changed around alterTags in that we now need an empty check instead?

  2. +++ b/core/lib/Drupal/Core/Database/Query/Select.php
    @@ -496,12 +496,23 @@ public function preExecute(SelectInterface $query = NULL) {
    +      $module_handler = FALSE;
    +      try {
    +        if (class_exists('Drupal')) {
    +          $module_handler = \Drupal::moduleHandler();
    +        }
    +      }
    +      catch (\Exception $e) {
    +        // No container yet, queries can't be altered.
    +      }
    +      if ($module_handler) {
    +        $module_handler->alter($hooks, $query);
    +      }
    

    Why do we check for the Drupal class, is it the container we should be testing for?

  3. +++ b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php
    @@ -43,41 +48,64 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
    +      foreach (array_keys($map) as $field) {
    +        if (!$schema->fieldExists($table, $field)) {
    +          $schema->addField($table, $field, ['type' => 'text']);
    +        }
    +      }
    

    Isn't this what crreateSchemaFromRow() does?

  4. +++ b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php
    @@ -85,11 +113,16 @@ protected function getIdMap($database_contents = array(), $connection_options =
    +    if ($this->database instanceof SQLiteConnection) {
    +      $defaults['source_row_status'] = (string) $defaults['source_row_status'];
    +      $defaults['rollback_action'] = (string) $defaults['rollback_action'];
    +    }
    

    This could do with a comment, it just casts the values to a string?

  5. +++ b/core/modules/migrate/tests/src/Unit/MigrateTestCase.php
    @@ -54,21 +55,54 @@ protected function getMigration() {
    +      throw new \Exception('pdo_sqlite extension is required.');
    

    So the tests only work with the pdo_sqlite extension?

phenaproxima’s picture

There is a ton of changing array syntax from array() to [] in the patch which is a shame since its large enough as it is.

I agree with this, pointless noise in the patch which in turn made it much harder to review.

I apologize. It was requested, and encouraged, and I did it. I'll be more careful in the future.

What changed around alterTags in that we now need an empty check instead?

Why do we check for the Drupal class, is it the container we should be testing for?

@chx can answer these better than I can, since he made those changes.

Isn't this what createSchemaFromRow() does?

Yes, except the one you quoted only creates fields that don't already exist in the already-existing table. I could probably streamline this, though.

This could do with a comment, it just casts the values to a string?

It's to get around a weird thing that PDO does. @chx and I looked into it the other day. I agree that a comment is very much in order.

So the tests only work with the pdo_sqlite extension?

No -- only getDatabase() requires the pdo_sqlite extension. Which makes sense to me, since if a test is calling getDatabase(), it needs...a database. If a test never calls getDatabase(), it will never hit that requirement.

phenaproxima’s picture

Added a comment about the string casts.

Regarding #28.1, at the moment there's not much difference between isset() and empty(), because $this->alterTags is undefined until someone adds a tag. Since empty() does an implicit isset() check, this is just a bit of future proofing for when alterTags is actually predefined on the Query base class.

And regarding #28.2, I have a feeling (but I'm not certain) that It's because DBTNG may eventually be split out as a component. (Confirmed by @chx.)

phenaproxima’s picture

Ugh. Patch this time.

chx’s picture

To quote #23

DBTNG being Drupal independent was an important design consideration

I am not sure what else needs to be said? Hardwiring Drupal:: there is probably a bug and just calling moduleHandler without try-catch, as explained, again, in #23 is definitely a bug.

phenaproxima’s picture

FileSize
123.33 KB

As per discussion on IRC, reverted @chx's changes in #23. They should be addressed when DBTNG is split out into a component, but it's out of scope for this issue.

EDIT: I didn't include an interdiff because the changes are the exact inverse of #23. In fact, I used git apply --reverse interdiff-23.txt to generate this patch. :)

benjy’s picture

Status: Needs review » Reviewed & tested by the community

OK reviewed again, i'm glad we removed the stuff from DBTNG from this patch. I think this is ready and a good clean-up.

alexpott’s picture

Status: Reviewed & tested by the community » Needs review
+++ b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php
@@ -698,18 +742,32 @@ public function testDestroy() {
-    $id_map = $this->getIdMap(array(), array('database' => 'source_database'));
-    $qualified_map_table = $id_map->getQualifiedMapTableName();
-    $this->assertEquals('source_database.migrate_map_sql_idmap_test', $qualified_map_table);
+    $this->database = $this->getDatabase([] , ['database' => 'source_database']);
+
+    if ($this->database->driver() == 'sqlite') {
+      $this->markTestSkipped('The SQLite driver does not support the DATABASE.TABLE format.');
+    }
+    else {

Well this is always going to be skipped now. Since the database is sqlite. What's the point of having this test here? Should we move it to a KernelTestBase test? Also what does this mean about migrating from Drupal installations that use SQLite?

phenaproxima’s picture

FileSize
122.92 KB
1010 bytes

As per discussion with @alexpott on IRC, removed the testGetQualifiedMapTableNoPrefix() method. We'll restore it when this test case is converted to a kernel test, after #2304461: KernelTestBaseTNG™ lands. (Followup issue forthcoming.)

phenaproxima’s picture

phenaproxima’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC, with @alexpott's blessing.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

618 insertions, 2412 deletions. nice work! Less to maintain for all of us. Committed fa2a7a5 and pushed to 8.0.x. Thanks!

Migrate is not under beta evaluation.

  • alexpott committed fa2a7a5 on 8.0.x
    Issue #2499835 by phenaproxima, chx, benjy: Remove broken Fake DB driver
    
bzrudi71’s picture

Nice cleanup! Sadly, this causes a new fail in MigrateSqlIdMapTest for all non MySQL databases (PG-bot / SQLite-bot). Do we just need a follow-up here or new issues for both DB's?
Drupal\Tests\migrate\Unit\MigrateSqlIdMapTest 28 passes 1 fails

alexpott’s picture

@bzrudi71 let's handle this in a new issue.

alexpott’s picture

Status: Fixed » Closed (fixed)

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

fgm’s picture

I would say this needs a change record, but I cannot set this status, for some reason : this class was there for a long time, and initializing this SQLite connection uses significantly more code than initializing the FakeConnection did, so there is a risk to have the initialization code be replicated in various guises.

I met this problem in the MongoDB driver, FWIW.

benjy’s picture

@fgm, thanks for that, do you want to write a change record and post it here?