diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php
index 05b0784..212fc09 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php
@@ -2,377 +2,500 @@
 
 namespace Drupal\Tests\Core\Entity;
 
-use Drupal\Core\Config\Entity\ConfigEntityInterface;
-use Drupal\Core\DependencyInjection\ContainerBuilder;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Language\Language;
-use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Entity\Entity;
+use Drupal\Core\Entity\EntityMalformedException;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Exception\UndefinedLinkTemplateException;
+use Drupal\Core\Entity\RevisionableInterface;
+use Drupal\Core\Url;
 use Drupal\Tests\UnitTestCase;
 
 /**
+ * Tests URL handling of the \Drupal\Core\Entity\Entity class.
+ *
  * @coversDefaultClass \Drupal\Core\Entity\Entity
+ *
  * @group Entity
  */
 class EntityUrlTest extends UnitTestCase {
 
   /**
-   * The mocked entity manager.
+   * The entity manager mock used in this test.
    *
-   * @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   * @var \Prophecy\Prophecy\ProphecyInterface|\Drupal\Core\Entity\EntityManagerInterface
    */
   protected $entityManager;
 
   /**
-   * The mocked URL generator.
+   * The ID of the entity type used in this test.
    *
-   * @var \Drupal\Core\Routing\UrlGeneratorInterface|\PHPUnit_Framework_MockObject_MockObject
+   * @var string
    */
-  protected $urlGenerator;
+  protected $entityTypeId = 'test_entity';
 
   /**
-   * {@inheritdoc}
+   * The entity type mock used in this test.
+   *
+   * @var \Prophecy\Prophecy\ProphecyInterface|\Drupal\Core\Entity\EntityTypeInterface
    */
-  protected function setUp() {
-    parent::setUp();
+  protected $entityType;
 
-    $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
-    $this->urlGenerator = $this->getMock('Drupal\Core\Routing\UrlGeneratorInterface');
+  /**
+   * The ID of the entity used in this test.
+   *
+   * @var int
+   */
+  protected $entityId = 1;
 
-    $container = new ContainerBuilder();
-    $container->set('entity.manager', $this->entityManager);
-    $container->set('url_generator', $this->urlGenerator);
-    \Drupal::setContainer($container);
-  }
+  /**
+   * The revision ID of the entity used in this test.
+   *
+   * @var int
+   */
+  protected $revisionId = 2;
 
   /**
-   * Tests the urlInfo() method.
+   * The language code of the entity used in this test.
    *
-   * Note that urlInfo() is a deprecated alias for toUrl().
-   * See testToUrl().
+   * @var string
+   */
+  protected $langcode = 'en';
+
+  /**
+   * Indicator for default revisions.
    *
-   * @covers ::urlInfo
+   * @var true
+   */
+  const DEFAULT_REVISION = TRUE;
+
+  /**
+   * Indicator for non-default revisions.
    *
-   * @dataProvider providerTestToUrl
+   * @var false
    */
-  public function testUrlInfo($entity_class, $link_template, $expected, $langcode = NULL) {
-    /** @var \Drupal\Core\Entity\EntityInterface $entity */
-    $entity = $this->getMockForAbstractClass($entity_class, array(array('id' => 'test_entity_id'), 'test_entity_type'));
-    $uri = $this->getTestUrlInfo($entity, $link_template, [], $langcode);
+  const NON_DEFAULT_REVISION = FALSE;
 
-    $this->assertSame($expected, $uri->getRouteName());
-    $this->assertSame($entity, $uri->getOption('entity'));
+  /**
+   * Tests the toUrl() method without an entity ID.
+   *
+   * @covers ::toUrl
+   */
+  public function testToUrlNoId() {
+    $entity = $this->getEntity(Entity::class, []);
 
-    if ($langcode) {
-      $this->assertEquals($langcode, $uri->getOption('language')->getId());
-    }
-    else {
-      if ($entity instanceof ConfigEntityInterface) {
-        // Config entities do not provide a language with their URIs.
-        $this->assertEquals(NULL, $uri->getOption('language'));
-      }
-      else {
-        $this->assertEquals(LanguageInterface::LANGCODE_NOT_SPECIFIED, $uri->getOption('language')->getId());
-      }
-    }
+    $this->setExpectedException(EntityMalformedException::class, 'The "' . $this->entityTypeId . '" entity cannot have a URI as it does not have an ID');
+    $entity->toUrl();
   }
+
   /**
-   * Tests the toUrl() method.
+   * Tests the toUrl() method with simple link templates.
+   *
+   * @param string $link_template
+   *   The link template to test.
+   * @param string $expected_route_name
+   *   The expected route name of the generated URL.
+   *
+   * @dataProvider providerTestToUrlLinkTemplates
    *
    * @covers ::toUrl
+   * @covers ::linkTemplates
+   * @covers ::urlRouteParameters
+   */
+  public function testToUrlLinkTemplates($link_template, $expected_route_name) {
+    $values = ['id' => $this->entityId, 'langcode' => $this->langcode];
+    $entity = $this->getEntity(Entity::class, $values);
+    $this->registerLinkTemplate($link_template);
+
+    /** @var \Drupal\Core\Url $url */
+    $url = $entity->toUrl($link_template);
+    // The entity ID is the sole route parameter for the link templates tested
+    // here.
+    $this->assertUrl($expected_route_name, ['test_entity' => $this->entityId], $entity, TRUE, $url);
+  }
+
+  /**
+   * Provides data for testToUrlLinkTemplates().
    *
-   * @dataProvider providerTestToUrl
+   * @return array
+   *   An array of test cases for testToUrlLinkTemplates().
    */
-  public function testToUrl($entity_class, $link_template, $expected, $langcode = NULL) {
-    /** @var \Drupal\Core\Entity\EntityInterface $entity */
-    $entity = $this->getMockForAbstractClass($entity_class, array(array('id' => 'test_entity_id'), 'test_entity_type'));
-    $uri = $this->getTestToUrl($entity, $link_template, [], $langcode);
+  public function providerTestToUrlLinkTemplates() {
+    $test_cases = [];
 
-    $this->assertSame($expected, $uri->getRouteName());
-    $this->assertSame($entity, $uri->getOption('entity'));
+    $test_cases['canonical'] = ['canonical', 'entity.test_entity.canonical'];
+    $test_cases['version-history'] = ['version-history', 'entity.test_entity.version_history'];
+    $test_cases['edit-form'] = ['edit-form', 'entity.test_entity.edit_form'];
+    $test_cases['delete-form'] = ['delete-form', 'entity.test_entity.delete_form'];
 
-    if ($langcode) {
-      $this->assertEquals($langcode, $uri->getOption('language')->getId());
-    }
-    else {
-      if ($entity instanceof ConfigEntityInterface) {
-        // Config entities do not provide a language with their URIs.
-        $this->assertEquals(NULL, $uri->getOption('language'));
-      }
-      else {
-        $this->assertEquals(LanguageInterface::LANGCODE_NOT_SPECIFIED, $uri->getOption('language')->getId());
-      }
-    }
+    return $test_cases;
   }
 
   /**
-   * Tests for Entity::toUrl() exercising different language options.
+   * Tests the toUrl() method with the 'revision' link template.
+   *
+   * @param bool $is_default_revision
+   *   Whether or not the mock entity should be the default revision.
+   * @param string $link_template
+   *   The link template to test.
+   * @param string $expected_route_name
+   *   The expected route name of the generated URL.
+   * @param array $expected_route_parameters
+   *   The expected route parameters of the generated URL.
+   *
+   * @dataProvider providerTestToUrlLinkTemplateRevision
    *
    * @covers ::toUrl
+   * @covers ::linkTemplates
+   * @covers ::urlRouteParameters
    */
-  public function testToUrlWithSpecificLanguageInOptions() {
-    /** @var \Drupal\Core\Entity\EntityInterface $entity */
-    $entity = $this->getMockForAbstractClass('Drupal\Core\Entity\Entity', array(array('id' => 'test_entity_id'), 'test_entity_type'));
+  public function testToUrlLinkTemplateRevision($is_default_revision, $link_template, $expected_route_name, array $expected_route_parameters) {
+    $values = ['id' => $this->entityId, 'langcode' => $this->langcode];
+    $entity = $this->getEntity(RevisionableEntity::class, $values);
+    $entity->method('getRevisionId')->willReturn($this->revisionId);
+    $entity->method('isDefaultRevision')->willReturn($is_default_revision);
+    $this->registerLinkTemplate($link_template);
+    // Even though this is tested with both the 'canonical' and the 'revision'
+    // template registered with the entity, we always ask for the 'revision'
+    // link template, to test that it falls back to the 'canonical' link
+    // template in case of the default revision.
+    /** @var \Drupal\Core\Url $url */
+    $url = $entity->toUrl('revision');
+    $this->assertUrl($expected_route_name, $expected_route_parameters, $entity, TRUE, $url);
 
-    // Ensure that a specified language overrides the current translation
-    // language.
-    $uri = $this->getTestToUrl($entity, 'edit-form', [], 'en');
-    $this->assertEquals('en', $uri->getOption('language')->getId());
-
-    $uri = $this->getTestToUrl($entity, 'edit-form', ['language' => new Language(['id' => 'fr'])], 'en');
-    $this->assertEquals('fr', $uri->getOption('language')->getId());
   }
 
   /**
-   * Provides test data for testUrlInfo().
+   * Provides data for testToUrlLinkTemplateRevision().
+   *
+   * @return array
+   *   An array of test cases for testToUrlLinkTemplateRevision().
    */
-  public function providerTestToUrl() {
-    return array(
-      array('Drupal\Core\Entity\Entity', 'edit-form', 'entity.test_entity_type.edit_form', NULL),
-      // Specify a langcode.
-      array('Drupal\Core\Entity\Entity', 'edit-form', 'entity.test_entity_type.edit_form', 'es'),
-      array('Drupal\Core\Entity\Entity', 'edit-form', 'entity.test_entity_type.edit_form', 'en'),
-      array('Drupal\Core\Config\Entity\ConfigEntityBase', 'edit-form', 'entity.test_entity_type.edit_form', NULL),
-      // Test that overriding the default $rel parameter works.
-      array('Drupal\Core\Config\Entity\ConfigEntityBase', FALSE, 'entity.test_entity_type.edit_form', NULL),
-    );
+  public function providerTestToUrlLinkTemplateRevision() {
+    $test_cases = [];
+
+    $route_parameters = ['test_entity' => $this->entityId];
+    $test_cases['default_revision'] = [static::DEFAULT_REVISION, 'canonical', 'entity.test_entity.canonical', $route_parameters];
+    // Add the revision ID to the expected route parameters.
+    $route_parameters['test_entity_revision'] = $this->revisionId;
+    $test_cases['non_default_revision'] = [static::NON_DEFAULT_REVISION, 'revision', 'entity.test_entity.revision', $route_parameters];
+
+    return $test_cases;
   }
 
   /**
-   * Tests the toUrl() method with an invalid link template.
+   * Tests the toUrl() method with the 'collection' link template.
    *
    * @covers ::toUrl
+   * @covers ::linkTemplates
+   * @covers ::urlRouteParameters
+   */
+  public function testToUrlLinkTemplateCollection() {
+    $entity = $this->getEntity(Entity::class, ['id' => $this->entityId]);
+    $link_template = 'collection';
+    $this->registerLinkTemplate($link_template);
+
+    /** @var \Drupal\Core\Url $url */
+    $url = $entity->toUrl($link_template);
+    $this->assertUrl('entity.test_entity.collection', [], $entity, FALSE, $url);
+  }
+
+  /**
+   * Tests the toUrl() method with neither link templates nor a URI callback.
    *
-   * @expectedException \Drupal\Core\Entity\Exception\UndefinedLinkTemplateException
+   * @param array $bundle_info
+   *   An array of bundle info to register.
+   * @param string $uri_callback
+   *   The entity type URI callback to register.
    *
-   * @expectedExceptionMessage No link template 'canonical' found for the 'test_entity_type' entity type
+   * @dataProvider providerTestToUrlUriCallbackUndefined
    *
-   * @dataProvider providerTestToUrlForInvalidLinkTemplate
+   * @covers ::toUrl
+   * @covers ::linkTemplates
    */
-  public function testToUrlForInvalidLinkTemplate($entity_class, $link_template) {
-    /** @var \Drupal\Core\Entity\EntityInterface $entity */
-    $entity = $this->getMockForAbstractClass($entity_class, array(array('id' => 'test_entity_id'), 'test_entity_type'));
-    $uri = $this->getTestToUrl($entity, $link_template);
+  public function testToUrlUriCallbackUndefined(array $bundle_info, $uri_callback) {
+    $entity = $this->getEntity(Entity::class, ['id' => $this->entityId]);
+
+    $this->registerBundleInfo($bundle_info);
+    $this->entityType->getUriCallback()->willReturn($uri_callback);
 
-    $this->assertEmpty($uri);
+    $link_template = 'canonical';
+    $this->setExpectedException(UndefinedLinkTemplateException::class, "No link template '$link_template' found for the '$this->entityTypeId' entity type");
+    $entity->toUrl($link_template);
   }
 
   /**
-   * Provides test data for testUrlInfoForInvalidLinkTemplate().
+   * Provides data for testToUrlUriCallbackUndefined().
+   *
+   * @return array
+   *   An array of test cases for testToUrlUriCallbackUndefined().
    */
-  public function providerTestToUrlForInvalidLinkTemplate() {
-    return array(
-      array('Drupal\Core\Entity\Entity', 'canonical'),
-      array('Drupal\Core\Entity\Entity', FALSE),
-      array('Drupal\Core\Config\Entity\ConfigEntityBase', 'canonical'),
-    );
+  public function providerTestToUrlUriCallbackUndefined() {
+    $test_cases = [];
+
+    $test_cases['no_callback'] = [[], NULL];
+    $test_cases['uri_callback'] = [[], 'not_a_callable'];
+    $test_cases['bundle_uri_callback'] = [['uri_callback' => 'not_a_callable'], NULL];
+
+    return $test_cases;
   }
 
   /**
-   * Creates a \Drupal\Core\Url object based on the entity and link template.
+   * Tests the toUrl() method with a URI callback.
    *
-   * Method urlInfo() is deprecated and replaced with toUrl().
-   * See also getTestToUrl().
+   * @param array $bundle_info
+   *   An array of bundle info to register.
+   * @param string $uri_callback
+   *   The entity type URI callback to register.
    *
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   The test entity.
-   * @param string $link_template
-   *   The link template.
-   * @param string $langcode
-   *   The langcode.
+   * @covers ::toUrl
+   * @covers ::linkTemplates
    *
-   * @return \Drupal\Core\Url
-   *   The URL for this entity's link template.
+   * @dataProvider providerTestToUrlUriCallback
    */
-  protected function getTestUrlInfo(EntityInterface $entity, $link_template, array $options = [], $langcode = NULL) {
-    $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
-    $entity_type->expects($this->any())
-      ->method('getLinkTemplates')
-      ->will($this->returnValue(array(
-        'edit-form' => 'test_entity_type.edit',
-      )));
-
-    if ($langcode) {
-      $entity->langcode = $langcode;
-    }
+  public function testToUrlUriCallback(array $bundle_info, $uri_callback) {
+    $entity = $this->getEntity(Entity::class, ['id' => $this->entityId]);
 
-    $this->entityManager
-      ->expects($this->any())
-      ->method('getDefinition')
-      ->with('test_entity_type')
-      ->will($this->returnValue($entity_type));
-
-    // If no link template is given, call without a value to test the default.
-    if ($link_template) {
-      $uri = $entity->urlInfo($link_template, $options);
-    }
-    else {
-      if ($entity instanceof ConfigEntityInterface) {
-        $uri = $entity->urlInfo('edit-form', $options);
-      }
-      else {
-        $uri = $entity->urlInfo('canonical', $options);
-      }
-    }
+    $this->registerBundleInfo($bundle_info);
+    $this->entityType->getUriCallback()->willReturn($uri_callback);
 
-    return $uri;
+    /** @var \Drupal\Core\Url $url */
+    $url = $entity->toUrl('canonical');
+    $this->assertUrl('<none>', [], $entity, TRUE, $url);
   }
 
   /**
-   * Creates a \Drupal\Core\Url object based on the entity and link template.
-   *
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   The test entity.
-   * @param string $link_template
-   *   The link template.
-   * @param string $langcode
-   *   The langcode.
+   * Provides data for testToUrlUriCallback().
    *
-   * @return \Drupal\Core\Url
-   *   The URL for this entity's link template.
+   * @return array
+   *   An array of test cases for testToUrlUriCallback().
    */
-  protected function getTestToUrl(EntityInterface $entity, $link_template, array $options = [], $langcode = NULL) {
-    $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
-    $entity_type->expects($this->any())
-      ->method('getLinkTemplates')
-      ->will($this->returnValue(array(
-        'edit-form' => 'test_entity_type.edit',
-      )));
-
-    if ($langcode) {
-      $entity->langcode = $langcode;
-    }
+  public function providerTestToUrlUriCallback() {
+    $test_cases = [];
 
-    $this->entityManager
-      ->expects($this->any())
-      ->method('getDefinition')
-      ->with('test_entity_type')
-      ->will($this->returnValue($entity_type));
-
-    // If no link template is given, call without a value to test the default.
-    if ($link_template) {
-      $uri = $entity->toUrl($link_template, $options);
-    }
-    else {
-      if ($entity instanceof ConfigEntityInterface) {
-        $uri = $entity->toUrl('edit-form', $options);
-      }
-      else {
-        $uri = $entity->toUrl('canonical', $options);
-      }
-    }
+    $uri_callback = function () { return Url::fromRoute('<none>'); };
+    $test_cases['uri_callback'] = [[], $uri_callback];
+    $test_cases['bundle_uri_callback'] = [['uri_callback' => $uri_callback], NULL];
 
-    return $uri;
+    return $test_cases;
   }
 
   /**
-   * Tests the toUrl() method when an entity is still "new".
+   * Tests the urlInfo() method.
    *
-   * @see \Drupal\Core\Entity\EntityInterface::isNew()
+   * @param string $rel
+   *   The link relation to test.
+   * @param array $options
+   *   An array of URL options to test with.
    *
-   * @covers ::toUrl
+   * @covers ::urlInfo
    *
-   * @expectedException \Drupal\Core\Entity\EntityMalformedException
+   * @dataProvider providerTestUrlInfo
    */
-  public function testToUrlForNewEntity() {
-    $entity = $this->getMockForAbstractClass('Drupal\Core\Entity\Entity', array(array(), 'test_entity_type'));
-    $entity->toUrl();
+  public function testUrlInfo($rel, $options) {
+    $entity = $this->getEntity(Entity::class, [], ['toUrl']);
+    $entity->expects($this->once())
+      ->method('toUrl')
+      ->with($rel, $options);
+
+    $entity->urlInfo($rel, $options);
   }
 
   /**
-   * Tests the url() method.
+   * Provides data for testUrlInfo().
+   *
+   * @return array
+   *   An array of test cases for testUrlInfo().
+   */
+  public function providerTestUrlInfo() {
+    $test_cases = [];
+
+    $test_cases['default'] = ['canonical', []];
+    $test_cases['with_option'] = ['canonical', ['absolute' => TRUE]];
+    $test_cases['revision'] = ['revision', []];
+
+    return $test_cases;
+  }
+
+  /**
+   * Tests the url() method without an entity ID.
+   *
+   * @param string $rel
+   *   The link relation to test.
    *
    * @covers ::url
+   * @covers ::hasLinkTemplate
+   * @covers ::linkTemplates
+   *
+   * @dataProvider providerTestUrl
    */
-  public function testUrl() {
-    $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
-    $entity_type->expects($this->any())
-      ->method('getLinkTemplates')
-      ->will($this->returnValue(array(
-        'canonical' => 'test_entity_type.view',
-      )));
+  public function testUrlEmpty($rel) {
+    $entity = $this->getEntity(Entity::class, []);
+    $this->assertEquals('', $entity->url($rel));
+  }
 
-    $this->entityManager
-      ->expects($this->any())
-      ->method('getDefinition')
-      ->with('test_entity_type')
-      ->will($this->returnValue($entity_type));
-
-    $invalid_entity = $this->getMockForAbstractClass('Drupal\Core\Entity\Entity', array(array(), 'test_entity_type'));
-    $this->assertSame('', $invalid_entity->url());
-
-    $no_link_entity = $this->getMockForAbstractClass('Drupal\Core\Entity\Entity', array(array('id' => 'test_entity_id'), 'test_entity_type'));
-    $this->assertSame('', $no_link_entity->url('banana'));
-
-    $valid_entity = $this->getMockForAbstractClass('Drupal\Core\Entity\Entity', array(array('id' => 'test_entity_id'), 'test_entity_type'));
-
-    $language = new Language(array('id' => LanguageInterface::LANGCODE_NOT_SPECIFIED));
-    $this->urlGenerator->expects($this->any())
-      ->method('generateFromRoute')
-      // Sadly returnValueMap() uses ===, see \PHPUnit_Framework_MockObject_Stub_ReturnValueMap::invoke
-      // so the $language object can't be compared directly.
-      ->willReturnCallback(function ($route_name, $route_parameters, $options) use ($language) {
-        if ($route_name === 'entity.test_entity_type.canonical' && $route_parameters === array('test_entity_type' => 'test_entity_id') && array_keys($options) === ['entity_type', 'entity', 'language'] && $options['language'] == $language) {
-          return '/entity/test_entity_type/test_entity_id';
-        }
-        if ($route_name === 'entity.test_entity_type.canonical' && $route_parameters === array('test_entity_type' => 'test_entity_id') && array_keys($options) === ['absolute', 'entity_type', 'entity', 'language'] && $options['language'] == $language) {
-          return 'http://drupal/entity/test_entity_type/test_entity_id';
-        }
-      });
-
-    $this->assertSame('/entity/test_entity_type/test_entity_id', $valid_entity->url());
-    $this->assertSame('http://drupal/entity/test_entity_type/test_entity_id', $valid_entity->url('canonical', array('absolute' => TRUE)));
+  /**
+   * Provides data for testUrlEmpty().
+   *
+   * @return array
+   *   An array of test cases for testUrlEmpty().
+   */
+  public function providerTestUrlEmpty() {
+    $test_cases = [];
+
+    $test_cases['default'] = ['canonical', []];
+    $test_cases['revision'] = ['revision', []];
+
+    return $test_cases;
   }
 
   /**
-   * Tests the retrieval of link templates.
+   * Tests the url() method.
    *
+   * @param string $rel
+   *   The link relation to test.
+   * @param array $options
+   *   An array of URL options to call url() with.
+   * @param array $default_options
+   *   An array of URL options that toUrl() should generate.
+   * @param array $expected_options
+   *   An array of combined URL options that should be set on the final URL.
+   *
+   * @covers ::url
    * @covers ::hasLinkTemplate
    * @covers ::linkTemplates
    *
-   * @dataProvider providerTestLinkTemplates
+   * @dataProvider providerTestUrl
    */
-  public function testLinkTemplates($override_templates, $expected) {
-    $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
-    $entity_type->expects($this->any())
-      ->method('getLinkTemplates')
-      ->will($this->returnValue(array(
-        'canonical' => 'test_entity_type.view',
-      )));
+  public function testUrl($rel, $options, $default_options, $expected_options) {
+    $entity = $this->getEntity(Entity::class, ['id' => $this->entityId], ['toUrl']);
+    $this->registerLinkTemplate($rel);
 
-    $this->entityManager
-      ->expects($this->any())
-      ->method('getDefinition')
-      ->with('test_entity_type')
-      ->will($this->returnValue($entity_type));
-
-    $entity = $this->getMockForAbstractClass('Drupal\Core\Entity\Entity', array(array('id' => 'test_entity_id'), 'test_entity_type'), '', TRUE, TRUE, TRUE, array('linkTemplates'));
-    $entity->expects($this->any())
-      ->method('linkTemplates')
-      ->will($this->returnCallback(function () use ($entity_type, $override_templates) {
-        $templates = $entity_type->getLinkTemplates();
-        if ($override_templates) {
-          $templates['bananas'] = 'test_entity_type.bananas';
-        }
-        return $templates;
-      }));
-    $this->assertSame($expected['canonical'], $entity->hasLinkTemplate('canonical'));
-    $this->assertSame($expected['bananas'], $entity->hasLinkTemplate('bananas'));
+    $uri = $this->prophesize(Url::class);
+    $uri->getOptions()->willReturn($default_options);
+    $uri->setOptions($expected_options)->shouldBeCalled();
+
+    $url_string = "/test-entity/{$this->entityId}/$rel";
+    $uri->toString()->willReturn($url_string);
+
+    $entity->expects($this->once())
+      ->method('toUrl')
+      ->with($rel)
+      ->willReturn($uri->reveal());
+
+    $this->assertEquals($url_string, $entity->url($rel, $options));
+  }
+
+  /**
+   * Provides data for testUrl().
+   *
+   * @return array
+   *   An array of test cases for testUrl().
+   */
+  public function providerTestUrl() {
+    $test_cases = [];
+
+    $test_cases['default'] = ['canonical', [], [], []];
+    $test_cases['revision'] = ['revision', [], [], []];
+    $test_cases['option'] = ['canonical', ['absolute' => TRUE], [], ['absolute' => TRUE]];
+    $test_cases['default_option'] = ['canonical', [], ['absolute' => TRUE], ['absolute' => TRUE]];
+    $test_cases['option_merge'] = ['canonical', ['absolute' => TRUE], ['entity_type' => $this->entityTypeId], ['absolute' => TRUE, 'entity_type' => $this->entityTypeId]];
+    $test_cases['option_override'] = ['canonical', ['absolute' => TRUE], ['absolute' => FALSE], ['absolute' => TRUE]];
+
+    return $test_cases;
+  }
+
+  /**
+   * Returns a mock entity for testing.
+   *
+   * @param string $class
+   *   The class name to mock. Should be \Drupal\Core\Entity\Entity or a
+   *   subclass.
+   * @param array $values
+   *   An array of entity values to construct the mock entity with.
+   * @param array $methods
+   *   (optional) An array of additional methods to mock on the entity object.
+   *   The getEntityType() and entityManager() methods are always mocked.
+   *
+   * @return \Drupal\Core\Entity\Entity|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected function getEntity($class, array $values, array $methods = []) {
+    $methods = array_merge($methods, ['getEntityType', 'entityManager']);
+
+    // Prophecy does not allow prophesizing abstract classes while actually
+    // calling their code. We use Prophecy below because that allows us to
+    // add method prophecies later while still revealing the prophecy now.
+    $entity = $this->getMockBuilder($class)
+      ->setConstructorArgs([$values, $this->entityTypeId])
+      ->setMethods($methods)
+      ->getMockForAbstractClass();
+
+    $this->entityType = $this->prophesize(EntityTypeInterface::class);
+    $this->entityType->getLinkTemplates()->willReturn([]);
+    $this->entityType->getKey('langcode')->willReturn(FALSE);
+    $entity->method('getEntityType')->willReturn($this->entityType->reveal());
+
+    $this->entityManager = $this->prophesize(EntityManagerInterface::class);
+    $entity->method('entityManager')->willReturn($this->entityManager->reveal());
+
+    return $entity;
+  }
+
+  /**
+   * Asserts that a given URL object matches the expectations.
+   *
+   * @param string $expected_route_name
+   *   The expected route name of the generated URL.
+   * @param array $expected_route_parameters
+   *   The expected route parameters of the generated URL.
+   * @param \Drupal\Core\Entity\Entity|\PHPUnit_Framework_MockObject_MockObject $entity
+   *   The entity that is expected to be set as a URL option.
+   * @param bool $has_language
+   *   Whether or not the URL is expected to have a language option.
+   * @param \Drupal\Core\Url $url
+   *   The URL option to make the assertions on.
+   */
+  protected function assertUrl($expected_route_name, array $expected_route_parameters, $entity, $has_language, Url $url) {
+    $this->assertEquals($expected_route_name, $url->getRouteName());
+    $this->assertEquals($expected_route_parameters, $url->getRouteParameters());
+    $this->assertEquals($this->entityTypeId, $url->getOption('entity_type'));
+    $this->assertEquals($entity, $url->getOption('entity'));
+    if ($has_language) {
+      $this->assertEquals($this->langcode, $url->getOption('language')->getId());
+    }
+    else {
+      $this->assertNull($url->getOption('language'));
+    }
+  }
+
+  /**
+   * Registers a link template for the mock entity.
+   *
+   * @param string $link_template
+   *   The link template to register.
+   */
+  protected function registerLinkTemplate($link_template) {
+    $link_templates = [
+      // The path is actually never used because we never invoke the URL
+      // generator but perform assertions on the URL object directly.
+      $link_template => "/test-entity/{test_entity}/$link_template",
+    ];
+    $this->entityType->getLinkTemplates()->willReturn($link_templates);
   }
 
   /**
-   * Provides test data for testLinkTemplates().
+   * Registers bundle information for the mock entity type.
+   *
+   * @param array $bundle_info
+   *   The bundle information to register.
    */
-  public function providerTestLinkTemplates() {
-    return array(
-      array(FALSE, array(
-        'canonical' => TRUE,
-        'bananas' => FALSE,
-      )),
-      array(TRUE, array(
-        'canonical' => TRUE,
-        'bananas' => TRUE,
-      )),
-    );
+  protected function registerBundleInfo($bundle_info) {
+    $this->entityManager
+      ->getBundleInfo($this->entityTypeId)
+      ->willReturn([$this->entityTypeId => $bundle_info])
+    ;
   }
 
 }
+
+abstract class RevisionableEntity extends Entity implements RevisionableInterface {}
