diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index 5ce3ba9..380b804 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -48,16 +48,6 @@
 const COMMENT_ANONYMOUS_MUST_CONTACT = 2;
 
 /**
- * Comment form should be displayed on a separate page.
- */
-const COMMENT_FORM_SEPARATE_PAGE = 0;
-
-/**
- * Comment form should be shown below post or list of comments.
- */
-const COMMENT_FORM_BELOW = 1;
-
-/**
  * The time cutoff for comments marked as read for entity types other node.
  *
  * Comments changed before this time are always marked as read.
@@ -257,151 +247,7 @@ function comment_node_links_alter(array &$node_links, NodeInterface $node, array
   // @todo Make this configurable from the formatter see
   //   http://drupal.org/node/1901110
 
-  $view_mode = $context['view_mode'];
-  if ($view_mode == 'search_index' || $view_mode == 'search_result' || $view_mode == 'print') {
-    // Do not add any links if the node displayed for:
-    // - search indexing.
-    // - constructing a search result excerpt.
-    // - print.
-    return;
-  }
-
-  $fields = \Drupal::service('comment.manager')->getFields('node');
-  $current_user = \Drupal::currentUser();
-  foreach ($fields as $field_name => $detail) {
-    // Skip fields that the node does not have.
-    if (!$node->hasField($field_name)) {
-      continue;
-    }
-    $links = array();
-    $commenting_status = $node->get($field_name)->status;
-    if ($commenting_status) {
-      $field_definition = $node->getFieldDefinition($field_name);
-      // Node have commenting open or close.
-      if ($view_mode == 'rss') {
-        // Add a comments RSS element which is a URL to the comments of this node.
-        $options = array(
-          'fragment' => 'comments',
-          'absolute' => TRUE,
-        );
-        $node->rss_elements[] = array(
-          'key' => 'comments',
-          'value' => $node->url('canonical', $options),
-        );
-      }
-      elseif ($view_mode == 'teaser') {
-        // Teaser view: display the number of comments that have been posted,
-        // or a link to add new comments if the user has permission, the node
-        // is open to new comments, and there currently are none.
-        if ($current_user->hasPermission('access comments')) {
-          if (!empty($node->get($field_name)->comment_count)) {
-            $links['comment-comments'] = array(
-              'title' => format_plural($node->get($field_name)->comment_count, '1 comment', '@count comments'),
-              'attributes' => array('title' => t('Jump to the first comment of this posting.')),
-              'fragment' => 'comments',
-              'html' => TRUE,
-            ) + $node->urlInfo()->toArray();
-            if (\Drupal::moduleHandler()->moduleExists('history')) {
-              $links['comment-new-comments'] = array(
-                'title' => '',
-                'href' => '',
-                'attributes' => array(
-                  'class' => 'hidden',
-                  'title' => t('Jump to the first new comment of this posting.'),
-                  'data-history-node-last-comment-timestamp' => $node->get($field_name)->last_comment_timestamp,
-                  'data-history-node-field-name' => $field_name,
-                ),
-                'html' => TRUE,
-              );
-            }
-          }
-        }
-        // Provide a link to new comment form.
-        if ($commenting_status == CommentItemInterface::OPEN) {
-          $comment_form_location = $field_definition->getSetting('form_location');
-          if ($current_user->hasPermission('post comments')) {
-            $links['comment-add'] = array(
-              'title' => t('Add new comment'),
-              'language' => $node->language(),
-              'attributes' => array('title' => t('Add a new comment to this page.')),
-              'fragment' => 'comment-form',
-            );
-            if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
-              $links['comment-add']['route_name'] = 'comment.reply';
-              $links['comment-add']['route_parameters'] = array(
-                'entity_type' => $node->getEntityTypeId(),
-                'entity_id' => $node->id(),
-                'field_name' => $field_name,
-              );
-            }
-            else {
-              $links['comment-add'] += $node->urlInfo()->toArray();
-            }
-          }
-          elseif (\Drupal::currentUser()->isAnonymous()) {
-            $links['comment-forbidden'] = array(
-              'title' => \Drupal::service('comment.manager')->forbiddenMessage($node, $field_name),
-              'html' => TRUE,
-            );
-          }
-        }
-      }
-      else {
-        // Node in other view modes: add a "post comment" link if the user is
-        // allowed to post comments and if this node is allowing new comments.
-        if ($commenting_status == CommentItemInterface::OPEN) {
-          $comment_form_location = $field_definition->getSetting('form_location');
-          if ($current_user->hasPermission('post comments')) {
-            // Show the "post comment" link if the form is on another page, or
-            // if there are existing comments that the link will skip past.
-            if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE || (!empty($node->get($field_name)->comment_count) && $current_user->hasPermission('access comments'))) {
-              $links['comment-add'] = array(
-                'title' => t('Add new comment'),
-                'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
-                'fragment' => 'comment-form',
-              );
-              if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
-                $links['comment-add']['route_name'] = 'comment.reply';
-                $links['comment-add']['route_parameters'] = array(
-                  'entity_type' => $node->getEntityTypeId(),
-                  'entity_id' => $node->id(),
-                  'field_name' => $field_name,
-                );
-              }
-              else {
-                $links['comment-add'] += $node->urlInfo()->toArray();
-              }
-            }
-          }
-          elseif (\Drupal::currentUser()->isAnonymous()) {
-            $links['comment-forbidden'] = array(
-              'title' => \Drupal::service('comment.manager')->forbiddenMessage($node, $field_name),
-              'html' => TRUE,
-            );
-          }
-        }
-      }
-    }
-
-    if (!empty($links)) {
-      $node_links['comment__' . $field_name] = array(
-        '#theme' => 'links__entity__comment__' . $field_name,
-        '#links' => $links,
-        '#attributes' => array('class' => array('links', 'inline')),
-      );
-      if ($view_mode == 'teaser' && \Drupal::moduleHandler()->moduleExists('history') && \Drupal::currentUser()->isAuthenticated()) {
-        $node_links['comment__' . $field_name]['#attached']['library'][] = 'comment/drupal.node-new-comments-link';
-
-        // Embed the metadata for the "X new comments" link (if any) on this node.
-        $node_links['comment__' . $field_name]['#post_render_cache']['history_attach_timestamp'] = array(
-          array('node_id' => $node->id()),
-        );
-        $node_links['comment__' . $field_name]['#post_render_cache']['Drupal\comment\CommentViewBuilder::attachNewCommentsLinkMetadata'] = array(
-          array('entity_type' => $node->getEntityTypeId(), 'entity_id' => $node->id(), 'field_name' => $field_name),
-        );
-      }
-    }
-  }
+  \Drupal::service('comment.link_builder')->buildLinks($node_links, $node, $context);
 }
 
 /**
diff --git a/core/modules/comment/comment.services.yml b/core/modules/comment/comment.services.yml
index 18405ac..b1208b1 100644
--- a/core/modules/comment/comment.services.yml
+++ b/core/modules/comment/comment.services.yml
@@ -18,3 +18,7 @@ services:
   comment.post_render_cache:
     class: Drupal\comment\CommentPostRenderCache
     arguments: ['@entity.manager', '@entity.form_builder']
+
+  comment.link_builder:
+    class: Drupal\comment\CommentLinkBuilder
+    arguments: ['@current_user', '@comment.manager', '@module_handler', '@string_translation']
diff --git a/core/modules/comment/src/CommentLinkBuilder.php b/core/modules/comment/src/CommentLinkBuilder.php
new file mode 100644
index 0000000..779729d
--- /dev/null
+++ b/core/modules/comment/src/CommentLinkBuilder.php
@@ -0,0 +1,184 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\comment\CommentLinkBuilder.
+ */
+
+namespace Drupal\comment;
+
+use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Session\AccountProxyInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\node\NodeInterface;
+
+/**
+ * Defines a class for building markup for comment links on a commented entity.
+ *
+ * Comment links include 'login to post new comment', 'add new comment' etc.
+ */
+class CommentLinkBuilder implements CommentLinkBuilderInterface {
+
+  use StringTranslationTrait;
+
+  protected $currentUser;
+  protected $commentManager;
+  protected $moduleHandler;
+
+
+  public function __construct(AccountProxyInterface $current_user, CommentManagerInterface $comment_manager, ModuleHandlerInterface $module_handler, TranslationInterface $string_translation) {
+    $this->currentUser = $current_user;
+    $this->commentManager = $comment_manager;
+    $this->moduleHandler = $module_handler;
+    $this->stringTranslation = $string_translation;
+  }
+
+  public function buildLinks(array &$node_links, NodeInterface $node, array &$context) {
+    $view_mode = $context['view_mode'];
+    if ($view_mode == 'search_index' || $view_mode == 'search_result' || $view_mode == 'print') {
+      // Do not add any links if the node displayed for:
+      // - search indexing.
+      // - constructing a search result excerpt.
+      // - print.
+      return;
+    }
+
+    $fields = $this->commentManager->getFields('node');
+    foreach ($fields as $field_name => $detail) {
+      // Skip fields that the node does not have.
+      if (!$node->hasField($field_name)) {
+        continue;
+      }
+      $links = array();
+      $commenting_status = $node->get($field_name)->status;
+      if ($commenting_status) {
+        $field_definition = $node->getFieldDefinition($field_name);
+        // Node have commenting open or close.
+        if ($view_mode == 'rss') {
+          // Add a comments RSS element which is a URL to the comments of this node.
+          $options = array(
+            'fragment' => 'comments',
+            'absolute' => TRUE,
+          );
+          $node->rss_elements[] = array(
+            'key' => 'comments',
+            'value' => $node->url('canonical', $options),
+          );
+        }
+        elseif ($view_mode == 'teaser') {
+          // Teaser view: display the number of comments that have been posted,
+          // or a link to add new comments if the user has permission, the node
+          // is open to new comments, and there currently are none.
+          if ($this->currentUser->hasPermission('access comments')) {
+            if (!empty($node->get($field_name)->comment_count)) {
+              $links['comment-comments'] = array(
+                'title' => $this->formatPlural($node->get($field_name)->comment_count, '1 comment', '@count comments'),
+                'attributes' => array('title' => $this->t('Jump to the first comment of this posting.')),
+                'fragment' => 'comments',
+                'html' => TRUE,
+              ) + $node->urlInfo()->toArray();
+              if ($this->moduleHandler->moduleExists('history')) {
+                $links['comment-new-comments'] = array(
+                  'title' => '',
+                  'href' => '',
+                  'attributes' => array(
+                    'class' => 'hidden',
+                    'title' => $this->getStringTranslation()->translate('Jump to the first new comment of this posting.'),
+                    'data-history-node-last-comment-timestamp' => $node->get($field_name)->last_comment_timestamp,
+                    'data-history-node-field-name' => $field_name,
+                  ),
+                  'html' => TRUE,
+                );
+              }
+            }
+          }
+          // Provide a link to new comment form.
+          if ($commenting_status == CommentItemInterface::OPEN) {
+            $comment_form_location = $field_definition->getSetting('form_location');
+            if ($this->currentUser->hasPermission('post comments')) {
+              $links['comment-add'] = array(
+                'title' => $this->t('Add new comment'),
+                'language' => $node->language(),
+                'attributes' => array('title' => $this->t('Add a new comment to this page.')),
+                'fragment' => 'comment-form',
+              );
+              if ($comment_form_location == CommentItemInterface::FORM_SEPARATE_PAGE) {
+                $links['comment-add']['route_name'] = 'comment.reply';
+                $links['comment-add']['route_parameters'] = array(
+                  'entity_type' => $node->getEntityTypeId(),
+                  'entity_id' => $node->id(),
+                  'field_name' => $field_name,
+                );
+              }
+              else {
+                $links['comment-add'] += $node->urlInfo()->toArray();
+              }
+            }
+            elseif ($this->currentUser->isAnonymous()) {
+              $links['comment-forbidden'] = array(
+                'title' => $this->commentManager->forbiddenMessage($node, $field_name),
+                'html' => TRUE,
+              );
+            }
+          }
+        }
+        else {
+          // Node in other view modes: add a "post comment" link if the user is
+          // allowed to post comments and if this node is allowing new comments.
+          if ($commenting_status == CommentItemInterface::OPEN) {
+            $comment_form_location = $field_definition->getSetting('form_location');
+            if ($this->currentUser->hasPermission('post comments')) {
+              // Show the "post comment" link if the form is on another page, or
+              // if there are existing comments that the link will skip past.
+              if ($comment_form_location == CommentItemInterface::FORM_SEPARATE_PAGE || (!empty($node->get($field_name)->comment_count) && $this->currentUser->hasPermission('access comments'))) {
+                $links['comment-add'] = array(
+                  'title' => $this->t('Add new comment'),
+                  'attributes' => array('title' => $this->t('Share your thoughts and opinions related to this posting.')),
+                  'fragment' => 'comment-form',
+                );
+                if ($comment_form_location == CommentItemInterface::FORM_SEPARATE_PAGE) {
+                  $links['comment-add']['route_name'] = 'comment.reply';
+                  $links['comment-add']['route_parameters'] = array(
+                    'entity_type' => $node->getEntityTypeId(),
+                    'entity_id' => $node->id(),
+                    'field_name' => $field_name,
+                  );
+                }
+                else {
+                  $links['comment-add'] += $node->urlInfo()->toArray();
+                }
+              }
+            }
+            elseif ($this->currentUser->isAnonymous()) {
+              $links['comment-forbidden'] = array(
+                'title' => $this->commentManager->forbiddenMessage($node, $field_name),
+                'html' => TRUE,
+              );
+            }
+          }
+        }
+      }
+
+      if (!empty($links)) {
+        $node_links['comment__' . $field_name] = array(
+          '#theme' => 'links__entity__comment__' . $field_name,
+          '#links' => $links,
+          '#attributes' => array('class' => array('links', 'inline')),
+        );
+        if ($view_mode == 'teaser' && $this->moduleHandler->moduleExists('history') && $this->currentUser->isAuthenticated()) {
+          $node_links['comment__' . $field_name]['#attached']['library'][] = 'comment/drupal.node-new-comments-link';
+
+          // Embed the metadata for the "X new comments" link (if any) on this node.
+          $node_links['comment__' . $field_name]['#post_render_cache']['history_attach_timestamp'] = array(
+            array('node_id' => $node->id()),
+          );
+          $node_links['comment__' . $field_name]['#post_render_cache']['Drupal\comment\CommentViewBuilder::attachNewCommentsLinkMetadata'] = array(
+            array('entity_type' => $node->getEntityTypeId(), 'entity_id' => $node->id(), 'field_name' => $field_name),
+          );
+        }
+      }
+    }
+  }
+}
diff --git a/core/modules/comment/src/CommentLinkBuilderInterface.php b/core/modules/comment/src/CommentLinkBuilderInterface.php
new file mode 100644
index 0000000..19265f9
--- /dev/null
+++ b/core/modules/comment/src/CommentLinkBuilderInterface.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\comment\CommentLinkBuilderInterface.
+ */
+
+namespace Drupal\comment;
+
+use Drupal\node\NodeInterface;
+
+/**
+ * Defines an interface for building comment links on a commented entity.
+ *
+ * Comment links include 'login to post new comment', 'add new comment' etc.
+ */
+interface CommentLinkBuilderInterface {
+  public function buildLinks(array &$node_links, NodeInterface $node, array &$context);
+}
diff --git a/core/modules/comment/src/CommentManager.php b/core/modules/comment/src/CommentManager.php
index f8dac54..5c76abb 100644
--- a/core/modules/comment/src/CommentManager.php
+++ b/core/modules/comment/src/CommentManager.php
@@ -273,7 +273,7 @@ public function forbiddenMessage(EntityInterface $entity, $field_name) {
     if ($this->authenticatedCanPostComments) {
       // We cannot use drupal_get_destination() because these links
       // sometimes appear on /node and taxonomy listing pages.
-      if ($entity->get($field_name)->getFieldDefinition()->getSetting('form_location') == COMMENT_FORM_SEPARATE_PAGE) {
+      if ($entity->get($field_name)->getFieldDefinition()->getSetting('form_location') == CommentItemInterface::FORM_SEPARATE_PAGE) {
         $destination = array('destination' => 'comment/reply/' . $entity->getEntityTypeId() . '/' . $entity->id() . '/' . $field_name . '#comment-form');
       }
       else {
diff --git a/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php b/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php
index e5ad723..6234844 100644
--- a/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php
+++ b/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php
@@ -182,7 +182,7 @@ public function viewElements(FieldItemListInterface $items) {
 
       // Append comment form if the comments are open and the form is set to
       // display below the entity. Do not show the form for the print view mode.
-      if ($status == CommentItemInterface::OPEN && $comment_settings['form_location'] == COMMENT_FORM_BELOW && $this->viewMode != 'print') {
+      if ($status == CommentItemInterface::OPEN && $comment_settings['form_location'] == CommentItemInterface::FORM_BELOW && $this->viewMode != 'print') {
         // Only show the add comment form if the user has permission.
         if ($this->currentUser->hasPermission('post comments')) {
           // All users in the "anonymous" role can use the same form: it is fine
diff --git a/core/modules/comment/src/Plugin/Field/FieldType/CommentItem.php b/core/modules/comment/src/Plugin/Field/FieldType/CommentItem.php
index 57e41d8..b54564a 100644
--- a/core/modules/comment/src/Plugin/Field/FieldType/CommentItem.php
+++ b/core/modules/comment/src/Plugin/Field/FieldType/CommentItem.php
@@ -44,7 +44,7 @@ public static function defaultInstanceSettings() {
     return array(
       'default_mode' => CommentManagerInterface::COMMENT_MODE_THREADED,
       'per_page' => 50,
-      'form_location' => COMMENT_FORM_BELOW,
+      'form_location' => CommentItemInterface::FORM_BELOW,
       'anonymous' => COMMENT_ANONYMOUS_MAYNOT_CONTACT,
       'preview' => DRUPAL_OPTIONAL,
     ) + parent::defaultInstanceSettings();
diff --git a/core/modules/comment/src/Plugin/Field/FieldType/CommentItemInterface.php b/core/modules/comment/src/Plugin/Field/FieldType/CommentItemInterface.php
index f61d43d..4a67eb9 100644
--- a/core/modules/comment/src/Plugin/Field/FieldType/CommentItemInterface.php
+++ b/core/modules/comment/src/Plugin/Field/FieldType/CommentItemInterface.php
@@ -27,4 +27,14 @@
    */
   const OPEN = 2;
 
+  /**
+   * Comment form should be displayed on a separate page.
+   */
+  const FORM_SEPARATE_PAGE = 0;
+
+  /**
+   * Comment form should be shown below post or list of comments.
+   */
+  const FORM_BELOW = 1;
+
 }
diff --git a/core/modules/comment/src/Tests/CommentLinksTest.php b/core/modules/comment/src/Tests/CommentLinksTest.php
index fe4b5aa..e64a68c 100644
--- a/core/modules/comment/src/Tests/CommentLinksTest.php
+++ b/core/modules/comment/src/Tests/CommentLinksTest.php
@@ -7,18 +7,33 @@
 
 namespace Drupal\comment\Tests;
 
+use Drupal\comment\Plugin\Field\FieldType\CommentItem;
 use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\comment\CommentInterface;
 
 /**
- * Tests comment links based on environment configurations.
+ * Basic comment links tests to ensure markup present.
  *
  * @group comment
  */
 class CommentLinksTest extends CommentTestBase {
 
   /**
+   * Comment being tested.
+   *
+   * @var \Drupal\comment\CommentInterface
+   */
+  protected $comment;
+
+  /**
+   * Seen comments, array of comment IDs.
+   *
+   * @var array
+   */
+  protected $seen = array();
+
+  /**
    * Use the main node listing to test rendering on teasers.
    *
    * @var array
@@ -39,9 +54,9 @@ class CommentLinksTest extends CommentTestBase {
    * possible conditions and tests the expected appearance of comment links in
    * each environment.
    */
-  function testCommentLinks() {
+  public function testCommentLinks() {
     // Bartik theme alters comment links, so use a different theme.
-    theme_enable(array('stark'));
+    \Drupal::service('theme_handler')->enable(array('stark'));
     \Drupal::config('system.theme')
       ->set('default', 'stark')
       ->save();
@@ -51,265 +66,52 @@ function testCommentLinks() {
     $roles = $this->web_user->getRoles();
     entity_delete_multiple('user_role', array(reset($roles)));
 
-    // Matrix of possible environmental conditions and configuration settings.
-    // See setEnvironment() for details.
-    $conditions = array(
-      'authenticated'   => array(FALSE, TRUE),
-      'comment count'   => array(FALSE, TRUE),
-      'access comments' => array(0, 1),
-      'post comments'   => array(0, 1),
-      'form'            => array(COMMENT_FORM_BELOW, COMMENT_FORM_SEPARATE_PAGE),
-      // USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL is irrelevant for this
-      // test; there is only a difference between open and closed registration.
-      'user_register'   => array(USER_REGISTER_VISITORS, USER_REGISTER_ADMINISTRATORS_ONLY),
-      // @todo Complete test coverage for:
-      //'comments'        => array(CommentItemInterface::OPEN, CommentItemInterface::CLOSED, CommentInterface::_HIDDEN),
-      //// COMMENT_ANONYMOUS_MUST_CONTACT is irrelevant for this test.
-      //'contact '        => array(COMMENT_ANONYMOUS_MAY_CONTACT, COMMENT_ANONYMOUS_MAYNOT_CONTACT),
-    );
-
-    $environments = $this->generatePermutations($conditions);
-    foreach ($environments as $info) {
-      $this->assertCommentLinks($info);
-    }
-  }
-
-  /**
-   * Re-configures the environment, module settings, and user permissions.
-   *
-   * @param $info
-   *   An associative array describing the environment to setup:
-   *   - Environment conditions:
-   *     - authenticated: Boolean whether to test with $this->web_user or
-   *       anonymous.
-   *     - comment count: Boolean whether to test with a new/unread comment on
-   *       $this->node or no comments.
-   *   - Configuration settings:
-   *     - form: COMMENT_FORM_BELOW or COMMENT_FORM_SEPARATE_PAGE.
-   *     - user_register: USER_REGISTER_ADMINISTRATORS_ONLY or
-   *       USER_REGISTER_VISITORS.
-   *     - contact: COMMENT_ANONYMOUS_MAY_CONTACT or
-   *       COMMENT_ANONYMOUS_MAYNOT_CONTACT.
-   *     - comments: CommentItemInterface::OPEN, CommentItemInterface::CLOSED or
-   *       CommentItemInterface::HIDDEN.
-   *   - User permissions:
-   *     These are granted or revoked for the user, according to the
-   *     'authenticated' flag above. Pass 0 or 1 as parameter values. See
-   *     user_role_change_permissions().
-   *     - access comments
-   *     - post comments
-   *     - skip comment approval
-   *     - edit own comments
-   */
-  function setEnvironment(array $info) {
-    static $current;
-
-    // Apply defaults to initial environment.
-    if (!isset($current)) {
-      $current = array(
-        'authenticated' => FALSE,
-        'comment count' => FALSE,
-        'form' => COMMENT_FORM_BELOW,
-        'user_register' => USER_REGISTER_VISITORS,
-        'contact' => COMMENT_ANONYMOUS_MAY_CONTACT,
-        'comments' => CommentItemInterface::OPEN,
-        'access comments' => 0,
-        'post comments' => 0,
-        // Enabled by default, because it's irrelevant for this test.
-        'skip comment approval' => 1,
-        'edit own comments' => 0,
-      );
-    }
-    // Complete new environment with current environment.
-    $info = array_merge($current, $info);
-
-    // Change environment conditions.
-    if ($current['authenticated'] != $info['authenticated']) {
-      if ($this->loggedInUser) {
-        $this->drupalLogout();
-      }
-      else {
-        $this->drupalLogin($this->web_user);
-      }
-    }
-    if ($current['comment count'] != $info['comment count']) {
-      if ($info['comment count']) {
-        // Create a comment via CRUD API functionality, since
-        // $this->postComment() relies on actual user permissions.
-        $comment = entity_create('comment', array(
-          'cid' => NULL,
-          'entity_id' => $this->node->id(),
-          'entity_type' => 'node',
-          'field_name' => 'comment',
-          'pid' => 0,
-          'uid' => 0,
-          'status' => CommentInterface::PUBLISHED,
-          'subject' => $this->randomMachineName(),
-          'hostname' => '127.0.0.1',
-          'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
-          'comment_body' => array(LanguageInterface::LANGCODE_NOT_SPECIFIED => array($this->randomMachineName())),
-        ));
-        $comment->save();
-        $this->comment = $comment;
-      }
-      else {
-        $cids = db_query("SELECT cid FROM {comment}")->fetchCol();
-        entity_delete_multiple('comment', $cids);
-        unset($this->comment);
-      }
-    }
+    // Create a comment via CRUD API functionality, since
+    // $this->postComment() relies on actual user permissions.
+    $comment = entity_create('comment', array(
+      'cid' => NULL,
+      'entity_id' => $this->node->id(),
+      'entity_type' => 'node',
+      'field_name' => 'comment',
+      'pid' => 0,
+      'uid' => 0,
+      'status' => CommentInterface::PUBLISHED,
+      'subject' => $this->randomMachineName(),
+      'hostname' => '127.0.0.1',
+      'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+      'comment_body' => array(LanguageInterface::LANGCODE_NOT_SPECIFIED => array($this->randomMachineName())),
+    ));
+    $comment->save();
+    $this->comment = $comment;
 
     // Change comment settings.
-    $this->setCommentSettings('form_location', $info['form'], 'Set comment form location');
-    $this->setCommentAnonymous($info['contact']);
-    if ($this->node->comment->status != $info['comments']) {
-      $this->node->comment = $info['comments'];
-      $this->node->save();
-    }
-
-    // Change user settings.
-    \Drupal::config('user.settings')->set('register', $info['user_register'])->save();
+    $this->setCommentSettings('form_location', CommentItem::FORM_BELOW, 'Set comment form location');
+    $this->setCommentAnonymous(TRUE);
+    $this->node->comment = CommentItem::OPEN;
+    $this->node->save();
 
     // Change user permissions.
-    $rid = ($this->loggedInUser ? DRUPAL_AUTHENTICATED_RID : DRUPAL_ANONYMOUS_RID);
-    $perms = array_intersect_key($info, array('access comments' => 1, 'post comments' => 1, 'skip comment approval' => 1, 'edit own comments' => 1));
-    user_role_change_permissions($rid, $perms);
-
-    // Output verbose debugging information.
-    // @see \Drupal\simpletest\TestBase::error()
-    $t_form = array(
-      COMMENT_FORM_BELOW => 'below',
-      COMMENT_FORM_SEPARATE_PAGE => 'separate page',
-    );
-    $t_contact = array(
-      COMMENT_ANONYMOUS_MAY_CONTACT => 'optional',
-      COMMENT_ANONYMOUS_MAYNOT_CONTACT => 'disabled',
-      COMMENT_ANONYMOUS_MUST_CONTACT => 'required',
-    );
-    $t_comments = array(
-      CommentItemInterface::OPEN => 'open',
-      CommentItemInterface::CLOSED => 'closed',
-      CommentItemInterface::HIDDEN => 'hidden',
+    $perms = array(
+      'access comments' => 1,
+      'post comments' => 1,
+      'skip comment approval' => 1,
+      'edit own comments' => 1,
     );
-    $verbose = $info;
-    $verbose['form'] = $t_form[$info['form']];
-    $verbose['contact'] = $t_contact[$info['contact']];
-    $verbose['comments'] = $t_comments[$info['comments']];
-    $message = t('Changed environment:<pre>@verbose</pre>', array(
-      '@verbose' => var_export($verbose, TRUE),
-    ));
-    $this->assert('debug', $message, 'Debug');
-
-    // Update current environment.
-    $current = $info;
-
-    return $info;
-  }
-
-  /**
-   * Asserts that comment links appear according to the passed environment.
-   *
-   * @param $info
-   *   An associative array describing the environment to pass to
-   *   setEnvironment().
-   */
-  function assertCommentLinks(array $info) {
-    $info = $this->setEnvironment($info);
+    user_role_change_permissions(DRUPAL_ANONYMOUS_RID, $perms);
 
     $nid = $this->node->id();
 
+    // Assert basic link is output, actual functionality is unit-tested in
+    // \Drupal\comment\Tests\CommentLinkBuilderTest.
     foreach (array('node', "node/$nid") as $path) {
       $this->drupalGet($path);
 
-      // User is allowed to view comments.
-      if ($info['access comments']) {
-        if ($path == '') {
-          // In teaser view, a link containing the comment count is always
-          // expected.
-          if ($info['comment count']) {
-            $this->assertLink(t('1 comment'));
-
-            // For logged in users, a link containing the amount of new/unread
-            // comments is expected.
-            // See important note about
-            // \Drupal::service('comment.manager')->getCountNewComments() below.
-            if ($this->loggedInUser && isset($this->comment) && !isset($this->comment->seen)) {
-              $this->assertLink(t('1 new comment'));
-              $this->comment->seen = TRUE;
-            }
-          }
-        }
-      }
-      else {
-        $this->assertNoLink(t('1 comment'));
-        $this->assertNoLink(t('1 new comment'));
-      }
-      // \Drupal::service('comment.manager')->getCountNewComments() is based on
-      // node views, so comments are marked as read when a node is viewed,
-      // regardless of whether we have access to comments.
-      if ($path == "node/$nid" && $this->loggedInUser && isset($this->comment)) {
-        $this->comment->seen = TRUE;
-      }
-
-      // User is not allowed to post comments.
-      if (!$info['post comments']) {
-        $this->assertNoLink('Add new comment');
-
-        // Anonymous users should see a note to log in or register in case
-        // authenticated users are allowed to post comments.
-        // @see \Drupal\comment\CommentManagerInterface::forbiddenMessage()
-        if (!$this->loggedInUser) {
-          if ($this->web_user->hasPermission('post comments')) {
-            // The note depends on whether users are actually able to register.
-            if ($info['user_register'] != USER_REGISTER_ADMINISTRATORS_ONLY) {
-              $this->assertText('Log in or register to post comments');
-            }
-            else {
-              $this->assertText('Log in to post comments');
-            }
-          }
-          else {
-            $this->assertNoText('Log in or register to post comments');
-            $this->assertNoText('Log in to post comments');
-          }
-        }
-      }
-      // User is allowed to post comments.
-      else {
-        $this->assertNoText('Log in or register to post comments');
-
-        // "Add new comment" is always expected, except when there are no
-        // comments or if the user cannot see them.
-        if ($path == "node/$nid" && $info['form'] == COMMENT_FORM_BELOW && (!$info['comment count'] || !$info['access comments'])) {
-          $this->assertNoLink('Add new comment');
-        }
-        else {
-          $this->assertLink('Add new comment');
-
-          // Verify that the "Add new comment" link points to the correct URL
-          // based on the comment form location configuration.
-          if ($info['form'] == COMMENT_FORM_SEPARATE_PAGE) {
-            $this->assertLinkByHref("comment/reply/node/$nid/comment#comment-form", 0, 'Comment form link destination is on a separate page.');
-            $this->assertNoLinkByHref("node/$nid#comment-form");
-          }
-          else {
-            $this->assertLinkByHref("node/$nid#comment-form", 0, 'Comment form link destination is on node.');
-            $this->assertNoLinkByHref("comment/reply/node/$nid/comment#comment-form");
-          }
-        }
-
-        // Also verify that the comment form appears according to the configured
-        // location.
-        if ($path == "node/$nid") {
-          $elements = $this->xpath('//form[@id=:id]', array(':id' => 'comment-form'));
-          if ($info['form'] == COMMENT_FORM_BELOW) {
-            $this->assertTrue(count($elements), 'Comment form found below.');
-          }
-          else {
-            $this->assertFalse(count($elements), 'Comment form not found below.');
-          }
-        }
+      // In teaser view, a link containing the comment count is always
+      // expected.
+      if ($path == 'node') {
+        $this->assertLink(t('1 comment'));
       }
+      $this->assertLink('Add new comment');
     }
   }
 
diff --git a/core/modules/comment/src/Tests/CommentTestBase.php b/core/modules/comment/src/Tests/CommentTestBase.php
index 6decc59..31155b1 100644
--- a/core/modules/comment/src/Tests/CommentTestBase.php
+++ b/core/modules/comment/src/Tests/CommentTestBase.php
@@ -10,6 +10,7 @@
 use Drupal\comment\Entity\CommentType;
 use Drupal\comment\Entity\Comment;
 use Drupal\comment\CommentInterface;
+use Drupal\comment\Plugin\Field\FieldType\CommentItem;
 use Drupal\field\Entity\FieldInstanceConfig;
 use Drupal\simpletest\WebTestBase;
 
@@ -265,7 +266,7 @@ public function setCommentPreview($mode, $field_name = 'comment') {
    *   Defaults to 'comment'.
    */
   public function setCommentForm($enabled, $field_name = 'comment') {
-    $this->setCommentSettings('form_location', ($enabled ? COMMENT_FORM_BELOW : COMMENT_FORM_SEPARATE_PAGE), 'Comment controls ' . ($enabled ? 'enabled' : 'disabled') . '.', $field_name);
+    $this->setCommentSettings('form_location', ($enabled ? CommentItem::FORM_BELOW : CommentItem::FORM_SEPARATE_PAGE), 'Comment controls ' . ($enabled ? 'enabled' : 'disabled') . '.', $field_name);
   }
 
   /**
diff --git a/core/modules/comment/tests/src/CommentLinkBuilderTest.php b/core/modules/comment/tests/src/CommentLinkBuilderTest.php
new file mode 100644
index 0000000..405ec6a
--- /dev/null
+++ b/core/modules/comment/tests/src/CommentLinkBuilderTest.php
@@ -0,0 +1,338 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\comment\Tests\CommentLinkBuilderTest.
+ */
+
+namespace Drupal\comment\Tests;
+
+use Drupal\comment\CommentLinkBuilder;
+use Drupal\comment\Plugin\Field\FieldType\CommentItem;
+use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
+use Drupal\node\NodeInterface;
+use Drupal\simpletest\TestBase;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\comment\CommentLinkBuilder
+ * @group comment
+ */
+class CommentLinkBuilderTest extends UnitTestCase {
+
+  /**
+   * Comment manager mock.
+   *
+   * @var \Drupal\comment\CommentManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $commentManager;
+
+  /**
+   * String translation mock.
+   *
+   * @var \Drupal\Core\StringTranslation\TranslationInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $stringTranslation;
+
+  /**
+   * Module handler mock.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $moduleHandler;
+
+  /**
+   * Current user proxy mock.
+   *
+   * @var \Drupal\Core\Session\AccountProxyInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $currentUser;
+
+  /**
+   * Timestamp used in test.
+   *
+   * @var int
+   */
+  protected $timestamp;
+
+  /**
+   * @var \Drupal\comment\CommentLinkBuilderInterface;
+   */
+  protected $commentLinkBuilder;
+
+  /**
+   * Prepares mocks for the test.
+   */
+  public function setUp() {
+    $this->commentManager = $this->getMock('\Drupal\comment\CommentManagerInterface');
+    $this->stringTranslation = $this->getMock('\Drupal\Core\StringTranslation\TranslationInterface');
+    $this->moduleHandler = $this->getMock('\Drupal\Core\Extension\ModuleHandlerInterface');
+    $this->currentUser = $this->getMock('\Drupal\Core\Session\AccountProxyInterface');
+    $this->commentLinkBuilder = new CommentLinkBuilder($this->currentUser, $this->commentManager, $this->moduleHandler, $this->stringTranslation);
+    $this->commentManager->expects($this->any())
+      ->method('getFields')
+      ->with('node')
+      ->willReturn(array(
+        'comment' => array(),
+      ));
+    $this->commentManager->expects($this->any())
+      ->method('forbiddenMessage')
+      ->willReturn("Can't let you do that Dave.");
+    $this->stringTranslation->expects($this->any())
+      ->method('translate')
+      ->willReturnArgument(0);
+    $this->stringTranslation->expects($this->any())
+      ->method('formatPlural')
+      ->willReturnArgument(1);
+  }
+
+  /**
+   * Test the buildLinks method.
+   *
+   * @param \Drupal\node\NodeInterface|\PHPUnit_Framework_MockObject_MockObject $node
+   *   Mock node.
+   * @param array $context
+   *   Context for the links.
+   * @param bool $has_access_comments
+   *   TRUE if the user has 'access comments' permission.
+   * @param bool $history_exists
+   *   TRUE if the history module exists.
+   * @param bool $has_post_comments
+   *   TRUE if the use has 'post comments' permission.
+   * @param bool $is_anonymous
+   *   TRUE if the user is anonymous.
+   * @param array $expected
+   *   Array of expected links keyed by link ID. Can be either string (link
+   *   title) or array of link properties.
+   *
+   * @dataProvider getLinkCombinations
+   *
+   * @covers ::buildLinks()
+   */
+  public function testCommentLinkBuilder(NodeInterface $node, $context, $has_access_comments, $history_exists, $has_post_comments, $is_anonymous, $expected) {
+    $this->moduleHandler->expects($this->any())
+      ->method('moduleExists')
+      ->with('history')
+      ->willReturn($history_exists);
+    $this->currentUser->expects($this->any())
+      ->method('hasPermission')
+      ->willReturnMap(array(
+        array('access comments', $has_access_comments),
+        array('post comments', $has_post_comments),
+      ));
+    $this->currentUser->expects($this->any())
+      ->method('isAuthenticated')
+      ->willReturn(!$is_anonymous);
+    $this->currentUser->expects($this->any())
+      ->method('isAnonymous')
+      ->willReturn($is_anonymous);
+    $links = array();
+    $this->commentLinkBuilder->buildLinks($links, $node, $context);
+    if (!empty($expected)) {
+      if (!empty($links)) {
+        foreach ($expected as $link => $detail) {
+          if (is_array($detail)) {
+            // Array of link attributes.
+            foreach ($detail as $key => $value) {
+              $this->assertEquals($links['comment__comment']['#links'][$link][$key], $value);
+            }
+          }
+          else {
+            // Just the title.
+            $this->assertEquals($links['comment__comment']['#links'][$link]['title'], $detail);
+          }
+        }
+      }
+      else {
+        $this->fail('Expected links but found none.');
+      }
+    }
+    else {
+      $this->assertSame($links, $expected);
+    }
+    if ($context['view_mode'] == 'rss' && $node->get('comment')->status) {
+      $found = FALSE;
+      if ($node->get('comment')->status) {
+        foreach ($node->rss_elements as $element) {
+          if ($element['key'] == 'comments') {
+            $found = TRUE;
+            break;
+          }
+        }
+      }
+      $this->assertTrue($found);
+    }
+  }
+
+  /**
+   * Data provider for ::testCommentLinkBuilder.
+   */
+  public function getLinkCombinations() {
+    $cases = array();
+    // No links should be created if the entity doesn't have the field.
+    $cases[] = array(
+      $this->getMockNode(FALSE, CommentItem::OPEN, CommentItem::FORM_BELOW, 1),
+      array('view_mode' => 'teaser'),
+      TRUE,
+      TRUE,
+      TRUE,
+      TRUE,
+      array(),
+    );
+    foreach (array('search_result', 'search_index', 'print') as $view_mode) {
+      // Nothing should be output in these view modes.
+      $cases[] = array(
+        $this->getMockNode(TRUE, CommentItem::OPEN, CommentItem::FORM_BELOW, 1),
+        array('view_mode' => $view_mode),
+        TRUE,
+        TRUE,
+        TRUE,
+        TRUE,
+        array(),
+      );
+    }
+    // All other combinations.
+    $combinations = array(
+      'is_anonymous' => array(FALSE, TRUE),
+      'comment_count' => array(0, 1),
+      'has_access_comments' => array(0, 1),
+      'history_exists' => array(FALSE, TRUE),
+      'has_post_comments'   => array(0, 1),
+      'form_location'            => array(CommentItemInterface::FORM_BELOW, CommentItemInterface::FORM_SEPARATE_PAGE),
+      'comments'        => array(
+        CommentItemInterface::OPEN,
+        CommentItemInterface::CLOSED,
+        CommentItemInterface::HIDDEN,
+      ),
+      'view_mode' => array(
+        'teaser', 'rss', 'full',
+      ),
+    );
+    $permutations = TestBase::generatePermutations($combinations);
+    foreach ($permutations as $combination) {
+      $case = array(
+        $this->getMockNode(TRUE, $combination['comments'], $combination['form_location'], $combination['comment_count']),
+        array('view_mode' => $combination['view_mode']),
+        $combination['has_access_comments'],
+        $combination['history_exists'],
+        $combination['has_post_comments'],
+        $combination['is_anonymous'],
+      );
+      $expected = array();
+      // When comments are enabled in teaser mode, and comments exist, and the
+      // user has access - we can output the comment count.
+      if ($combination['comments'] && $combination['view_mode'] == 'teaser' && $combination['comment_count'] && $combination['has_access_comments']) {
+        $expected['comment-comments'] = '1 comment';
+        // And if history module exists, we can show a 'new comments' link.
+        if ($combination['history_exists']) {
+          $expected['comment-new-comments'] = '';
+        }
+      }
+      // All view modes other than RSS.
+      if ($combination['view_mode'] != 'rss') {
+        // Where commenting is open.
+        if ($combination['comments'] == CommentItem::OPEN) {
+          // And the user has post-comments permission.
+          if ($combination['has_post_comments']) {
+            // If the view mode is teaser, or the user can access comments and
+            // comments exist or the form is on a separate page.
+            if ($combination['view_mode'] == 'teaser' || ($combination['has_access_comments'] && $combination['comment_count']) || $combination['form_location'] == CommentItem::FORM_SEPARATE_PAGE) {
+              // There should be a add comment link.
+              $expected['comment-add'] = array('title' => 'Add new comment');
+              if ($combination['form_location'] == CommentItem::FORM_BELOW) {
+                // On the same page.
+                $expected['comment-add']['route_name'] = 'node.view';
+              }
+              else {
+                // On a separate page.
+                $expected['comment-add']['route_name'] = 'comment.reply';
+              }
+            }
+          }
+          elseif ($combination['is_anonymous']) {
+            // Anonymous users get the forbidden message if the can't post
+            // comments.
+            $expected['comment-forbidden'] = "Can't let you do that Dave.";
+          }
+        }
+      }
+
+      $case[] = $expected;
+      $cases[] = $case;
+    }
+    return $cases;
+  }
+
+  /**
+   * Builds a mock node based on given scenario.
+   *
+   * @param bool $has_field
+   *   TRUE if the node has the 'comment' field.
+   * @param int $comment_status
+   *   One of CommentItem::OPEN|HIDDEN|CLOSED
+   * @param int $form_location
+   *   One of CommentItem::FORM_BELOW|FORM_SEPARATE_PAGE
+   * @param int $comment_count
+   *   Number of comments against the field.
+   *
+   * @return \Drupal\node\NodeInterface|\PHPUnit_Framework_MockObject_MockObject
+   *   Mock node for testing.
+   */
+  protected function getMockNode($has_field, $comment_status, $form_location, $comment_count) {
+    $node = $this->getMock('\Drupal\node\NodeInterface');
+    $node->expects($this->once())
+      ->method('hasField')
+      ->willReturn($has_field);
+
+    if (empty($this->timestamp)) {
+      $this->timestamp = time();
+    }
+    $field_item = (object) array(
+      'status' => $comment_status,
+      'comment_count' => $comment_count,
+      'last_comment_timestamp' => $this->timestamp,
+    );
+    $node->expects($this->any())
+      ->method('get')
+      ->with('comment')
+      ->willReturn($field_item);
+
+    $field_definition = $this->getMock('\Drupal\Core\Field\FieldDefinitionInterface');
+    $field_definition->expects($this->any())
+      ->method('getSetting')
+      ->with('form_location')
+      ->willReturn($form_location);
+    $node->expects($this->any())
+      ->method('getFieldDefinition')
+      ->with('comment')
+      ->willReturn($field_definition);
+
+    $node->expects($this->any())
+      ->method('language')
+      ->willReturn('und');
+
+    $node->expects($this->any())
+      ->method('getEntityTypeId')
+      ->willReturn('node');
+
+    $node->expects($this->any())
+      ->method('id')
+      ->willReturn(1);
+
+    $url = $this->getMockBuilder('\Drupal\Core\Url')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $url->expects($this->any())
+      ->method('toArray')
+      ->willReturn(array('route_name' => 'node.view'));
+    $node->expects($this->any())
+      ->method('urlInfo')
+      ->willReturn($url);
+    $node->expects($this->any())
+      ->method('url')
+      ->willReturn(array('route_name' => 'node.view'));
+
+    return $node;
+  }
+
+}
