diff --git a/core/modules/file/tests/src/Kernel/LinkTest.php b/core/modules/file/tests/src/Kernel/LinkTest.php
new file mode 100644
index 0000000..5e99846
--- /dev/null
+++ b/core/modules/file/tests/src/Kernel/LinkTest.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Drupal\Tests\file\Kernel;
+
+use Drupal\Core\Entity\Exception\UndefinedLinkTemplateException;
+use Drupal\Core\Url;
+use Drupal\file\Entity\File;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests \Drupal\file\Entity\File::link().
+ *
+ * @group file
+ */
+class LinkTest extends KernelTestBase {
+
+  /**
+   * Creates a file and returns its URI.
+   *
+   * @param string $file_path
+   *   (optional) A string specifying the file path. If it is NULL, then a
+   *   randomly named file will be created in the site's files directory.
+   * @param string $contents
+   *   (optional) The contents to save into the file. If it is NULL, then an
+   *   arbitrary string will be used.
+   * @param string $scheme
+   *   (optional) A string indicating the stream scheme to use. Drupal core
+   *   includes public, private, and temporary. The public wrapper is the
+   *   default.
+   *
+   * @return string
+   *   The file URI.
+   */
+  protected function createUri($file_path = NULL, $contents = NULL, $scheme = NULL) {
+    if (!isset($file_path)) {
+      $file_path = $this->randomMachineName();
+    }
+    if (!isset($scheme)) {
+      $scheme = file_default_scheme();
+    }
+    $file_path = $scheme . '://' . $file_path;
+
+    if (!isset($contents)) {
+      $contents = "file_put_contents() doesn't seem to appreciate empty strings so let's put in some data.";
+    }
+
+    file_put_contents($file_path, $contents);
+    $this->assertFileExists($file_path);
+
+    return $file_path;
+  }
+
+  /**
+   * Creates a file and saves it.
+   *
+   * @param string $file_path
+   *   (optional) A string specifying the file path. If it is NULL, then a
+   *   randomly named file will be created in the site's files directory.
+   * @param string $contents
+   *   (optional) The contents to save into the file. If it is NULL, then an
+   *   arbitrary string will be used.
+   * @param string $scheme
+   *   (optional) A string indicating the stream scheme to use. Drupal core
+   *   includes public, private, and temporary. The public wrapper is the
+   *   default.
+   *
+   * @return \Drupal\file\FileInterface
+   *   The file entity.
+   */
+  protected function createFile($file_path = NULL, $contents = NULL, $scheme = NULL) {
+    $file = File::create([
+      'uri' => $this->createUri($file_path, $contents, $scheme),
+      'uid' => 1,
+    ]);
+    $file->save();
+
+    return $file;
+  }
+
+  /**
+   * Creates a link for a public file and tests the public link.
+   */
+  public function testPublicLink() {
+    $file = $this->createFile('druplicon.txt', NULL, 'public');
+    $this->assertEquals($file->toLink()
+      ->toString(), "<a href=\"{$file->toUrl()->toString()}\">{$file->label()}</a>");
+  }
+
+  /**
+   * Tests toUrl() against a URI callback.
+   */
+  public function testUriCallback() {
+    $file = $this->createFile('druplicon.txt', NULL, 'public');
+    $expected = $file->toUrl()->toString() . '_test';
+    $file->getEntityType()->setUriCallback(function ($fileEntity) {
+      return Url::fromUri(file_create_url($fileEntity->getFileUri()) . '_test');
+    });
+    $actual = $file->toUrl()->toString();
+    $this->assertEquals($expected, $actual);
+  }
+
+  /**
+   * Tests that only canonical links are supported.
+   */
+  public function testNonCanonicalLink() {
+    $this->setExpectedException(UndefinedLinkTemplateException::class);
+
+    $file = $this->createFile('druplicon.txt', NULL, 'public');
+    $file->toLink('', 'notcanonical');
+  }
+
+}
