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..04326f1 100644
--- a/core/modules/comment/src/Tests/CommentLinksTest.php
+++ b/core/modules/comment/src/Tests/CommentLinksTest.php
@@ -19,6 +19,20 @@
 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 +53,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();
@@ -58,7 +72,7 @@ function testCommentLinks() {
       'comment count'   => array(FALSE, TRUE),
       'access comments' => array(0, 1),
       'post comments'   => array(0, 1),
-      'form'            => array(COMMENT_FORM_BELOW, COMMENT_FORM_SEPARATE_PAGE),
+      'form'            => array(CommentItemInterface::FORM_BELOW, CommentItemInterface::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),
@@ -72,12 +86,13 @@ function testCommentLinks() {
     foreach ($environments as $info) {
       $this->assertCommentLinks($info);
     }
+    $this->seen = array();
   }
 
   /**
    * Re-configures the environment, module settings, and user permissions.
    *
-   * @param $info
+   * @param array $info
    *   An associative array describing the environment to setup:
    *   - Environment conditions:
    *     - authenticated: Boolean whether to test with $this->web_user or
@@ -85,7 +100,7 @@ function testCommentLinks() {
    *     - 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.
+   *     - form: CommentItemInterface::FORM_BELOW or CommentItemInterface::FORM_SEPARATE_PAGE.
    *     - user_register: USER_REGISTER_ADMINISTRATORS_ONLY or
    *       USER_REGISTER_VISITORS.
    *     - contact: COMMENT_ANONYMOUS_MAY_CONTACT or
@@ -100,8 +115,11 @@ function testCommentLinks() {
    *     - post comments
    *     - skip comment approval
    *     - edit own comments
+   *
+   * @return array
+   *   Configured settings.
    */
-  function setEnvironment(array $info) {
+  protected function setEnvironment(array $info) {
     static $current;
 
     // Apply defaults to initial environment.
@@ -109,7 +127,7 @@ function setEnvironment(array $info) {
       $current = array(
         'authenticated' => FALSE,
         'comment count' => FALSE,
-        'form' => COMMENT_FORM_BELOW,
+        'form' => CommentItemInterface::FORM_BELOW,
         'user_register' => USER_REGISTER_VISITORS,
         'contact' => COMMENT_ANONYMOUS_MAY_CONTACT,
         'comments' => CommentItemInterface::OPEN,
@@ -172,14 +190,19 @@ function setEnvironment(array $info) {
 
     // 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));
+    $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',
+      CommentItemInterface::FORM_BELOW => 'below',
+      CommentItemInterface::FORM_SEPARATE_PAGE => 'separate page',
     );
     $t_contact = array(
       COMMENT_ANONYMOUS_MAY_CONTACT => 'optional',
@@ -209,11 +232,11 @@ function setEnvironment(array $info) {
   /**
    * Asserts that comment links appear according to the passed environment.
    *
-   * @param $info
+   * @param array $info
    *   An associative array describing the environment to pass to
    *   setEnvironment().
    */
-  function assertCommentLinks(array $info) {
+  protected function assertCommentLinks(array $info) {
     $info = $this->setEnvironment($info);
 
     $nid = $this->node->id();
@@ -233,9 +256,9 @@ function assertCommentLinks(array $info) {
             // comments is expected.
             // See important note about
             // \Drupal::service('comment.manager')->getCountNewComments() below.
-            if ($this->loggedInUser && isset($this->comment) && !isset($this->comment->seen)) {
+            if ($this->loggedInUser && isset($this->comment) && !isset($this->seen[$this->comment->id()])) {
               $this->assertLink(t('1 new comment'));
-              $this->comment->seen = TRUE;
+              $this->seen[$this->comment->id()] = TRUE;
             }
           }
         }
@@ -248,7 +271,7 @@ function assertCommentLinks(array $info) {
       // 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;
+        $this->seen[$this->comment->id()] = TRUE;
       }
 
       // User is not allowed to post comments.
@@ -280,7 +303,7 @@ function assertCommentLinks(array $info) {
 
         // "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'])) {
+        if ($path == "node/$nid" && $info['form'] == CommentItemInterface::FORM_BELOW && (!$info['comment count'] || !$info['access comments'])) {
           $this->assertNoLink('Add new comment');
         }
         else {
@@ -288,7 +311,7 @@ function assertCommentLinks(array $info) {
 
           // 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) {
+          if ($info['form'] == CommentItemInterface::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");
           }
@@ -302,7 +325,7 @@ function assertCommentLinks(array $info) {
         // location.
         if ($path == "node/$nid") {
           $elements = $this->xpath('//form[@id=:id]', array(':id' => 'comment-form'));
-          if ($info['form'] == COMMENT_FORM_BELOW) {
+          if ($info['form'] == CommentItemInterface::FORM_BELOW) {
             $this->assertTrue(count($elements), 'Comment form found below.');
           }
           else {
diff --git a/core/modules/comment/src/Tests/CommentTestBase.php b/core/modules/comment/src/Tests/CommentTestBase.php
index 6decc59..0a2bf17 100644
--- a/core/modules/comment/src/Tests/CommentTestBase.php
+++ b/core/modules/comment/src/Tests/CommentTestBase.php
@@ -265,7 +265,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 ? CommentItemInterface::FORM_BELOW : CommentItemInterface::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..62f6bff
--- /dev/null
+++ b/core/modules/comment/tests/src/CommentLinkBuilderTest.php
@@ -0,0 +1,277 @@
+<?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
+ * @group larowlan
+ */
+class CommentLinkBuilderTest extends UnitTestCase {
+
+  protected $commentManager;
+  protected $stringTranslation;
+  protected $moduleHandler;
+  protected $currentUser;
+  protected $timestamp;
+
+  /**
+   * @var \Drupal\comment\CommentLinkBuilderInterface;
+   */
+  protected $commentLinkBuilder;
+
+  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 $node
+   * @param $context
+   * @param $comment_status
+   * @param $has_access_comments
+   * @param $history_exists
+   * @param $has_post_comments
+   * @param $form_location
+   * @param $is_anonymous
+   * @param $expected
+   *
+   * @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') {
+      $found = FALSE;
+      if ($node->get('comment')->status) {
+        foreach ($node->rss_elements as $element) {
+          if ($element['key'] == 'comments') {
+            $found = TRUE;
+            break;
+          }
+        }
+      }
+      $this->assertTrue($found);
+    }
+  }
+
+  public function getLinkCombinations() {
+    $cases = array();
+    /**
+     * NodeInterface $node,
+     * $context,
+     * $has_access_comments,
+     * $history_exists,
+     * $has_post_comments,
+     * $is_anonymous,
+     * $expected*/
+    // 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 for these view modes.
+      $cases[] = array(
+        $this->getMockNode(TRUE, CommentItem::OPEN, CommentItem::FORM_BELOW, 1),
+        array('view_mode' => $view_mode),
+        TRUE,
+        TRUE,
+        TRUE,
+        TRUE,
+        array(),
+      );
+    }
+    $combinations = $conditions = 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();
+      if ($combination['view_mode'] == 'teaser') {
+        if ($combination['comment_count'] && $combination['has_access_comments']) {
+          $expected['comment-comments'] = '1 comment';
+        }
+        if ($combination['history_exists'] && $combination['comment_count']) {
+          $expected['comment-new-comments'] = '';
+        }
+      }
+      if ($combination['view_mode'] != 'rss') {
+        if ($combination['comments'] == CommentItem::OPEN) {
+          if ($combination['has_post_comments']) {
+            $expected['comment-add'] = array('title' => 'Add new comment');
+            if ($combination['form_location'] == CommentItem::FORM_BELOW) {
+              $expected['comment-add']['route_name'] = 'node.view';
+            }
+            else {
+              $expected['comment-add']['route_name'] = 'comment.reply';
+            }
+          }
+          elseif ($combination['is_anonymous']) {
+            $expected['comment-forbidden'] = "Can't let you do that Dave.";
+          }
+        }
+      }
+
+      $case[] = $expected;
+      $cases[] = $case;
+    }
+    return $cases;
+  }
+
+  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;
+  }
+
+}
