From 082fa66e71cad5b03738a5c3fe83e239362bc544 Mon Sep 17 00:00:00 2001
From: Nick Thomas <nick@gitlab.com>
Date: Thu, 10 Aug 2017 18:23:56 +0100
Subject: [PATCH] Use rspec-parameterized for table-based tests

---
 Gemfile                                       |   1 +
 Gemfile.lock                                  |  33 ++++
 doc/development/testing.md                    |  37 +++++
 spec/ee/spec/models/ee/issue_spec.rb          |  35 ++---
 spec/ee/spec/models/ee/merge_request_spec.rb  |  35 ++---
 .../models/ee/project_import_data_spec.rb     |  55 ++++---
 spec/ee/spec/models/ee/project_spec.rb        | 144 +++++++++---------
 spec/ee/spec/models/ssh_host_key_spec.rb      |  94 +++++++-----
 spec/spec_helper.rb                           |   1 +
 9 files changed, 260 insertions(+), 175 deletions(-)

diff --git a/Gemfile b/Gemfile
index c8d5fff061f4f..47260ca217a27 100644
--- a/Gemfile
+++ b/Gemfile
@@ -336,6 +336,7 @@ group :development, :test do
   gem 'spinach-rerun-reporter', '~> 0.0.2'
   gem 'rspec_profiling', '~> 0.0.5'
   gem 'rspec-set', '~> 0.1.3'
+  gem 'rspec-parameterized'
 
   # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
   gem 'minitest', '~> 5.7.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 4b2a191f39b98..0d1e0f7c41c2e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,6 +2,7 @@ GEM
   remote: https://rubygems.org/
   specs:
     RedCloth (4.3.2)
+    abstract_type (0.0.7)
     ace-rails-ap (4.1.2)
     actionmailer (4.2.8)
       actionpack (= 4.2.8)
@@ -41,6 +42,9 @@ GEM
       tzinfo (~> 1.1)
     acts-as-taggable-on (4.0.0)
       activerecord (>= 4.0)
+    adamantium (0.2.0)
+      ice_nine (~> 0.11.0)
+      memoizable (~> 0.4.0)
     addressable (2.3.8)
     after_commit_queue (1.3.0)
       activerecord (>= 3.0)
@@ -132,6 +136,9 @@ GEM
     coercible (1.0.0)
       descendants_tracker (~> 0.0.1)
     colorize (0.7.7)
+    concord (0.1.5)
+      adamantium (~> 0.2.0)
+      equalizer (~> 0.0.9)
     concurrent-ruby (1.0.5)
     concurrent-ruby-ext (1.0.5)
       concurrent-ruby (= 1.0.5)
@@ -489,6 +496,8 @@ GEM
       mime-types (>= 1.16, < 4)
     mail_room (0.9.1)
     memoist (0.15.0)
+    memoizable (0.4.2)
+      thread_safe (~> 0.3, >= 0.3.1)
     method_source (0.8.2)
     mime-types (2.99.3)
     mimemagic (0.3.0)
@@ -630,6 +639,11 @@ GEM
     premailer-rails (1.9.7)
       actionmailer (>= 3, < 6)
       premailer (~> 1.7, >= 1.7.9)
+    proc_to_ast (0.1.0)
+      coderay
+      parser
+      unparser
+    procto (0.0.3)
     prometheus-client-mmap (0.7.0.beta11)
       mmap2 (~> 2.2, >= 2.2.7)
     pry (0.10.4)
@@ -738,6 +752,10 @@ GEM
       chunky_png
     rqrcode-rails3 (0.1.7)
       rqrcode (>= 0.4.2)
+    rspec (3.6.0)
+      rspec-core (~> 3.6.0)
+      rspec-expectations (~> 3.6.0)
+      rspec-mocks (~> 3.6.0)
     rspec-core (3.6.0)
       rspec-support (~> 3.6.0)
     rspec-expectations (3.6.0)
@@ -746,6 +764,12 @@ GEM
     rspec-mocks (3.6.0)
       diff-lcs (>= 1.2.0, < 2.0)
       rspec-support (~> 3.6.0)
+    rspec-parameterized (0.4.0)
+      binding_of_caller
+      parser
+      proc_to_ast
+      rspec (>= 2.13, < 4)
+      unparser
     rspec-rails (3.6.0)
       actionpack (>= 3.0)
       activesupport (>= 3.0)
@@ -913,6 +937,14 @@ GEM
       get_process_mem (~> 0)
       unicorn (>= 4, < 6)
     uniform_notifier (1.10.0)
+    unparser (0.2.6)
+      abstract_type (~> 0.0.7)
+      adamantium (~> 0.2.0)
+      concord (~> 0.1.5)
+      diff-lcs (~> 1.3)
+      equalizer (~> 0.0.9)
+      parser (>= 2.3.1.2, < 2.5)
+      procto (~> 0.0.2)
     url_safe_base64 (0.2.2)
     validates_hostname (1.0.6)
       activerecord (>= 3.0)
@@ -1123,6 +1155,7 @@ DEPENDENCIES
   responders (~> 2.0)
   rouge (~> 2.0)
   rqrcode-rails3 (~> 0.1.7)
+  rspec-parameterized
   rspec-rails (~> 3.6.0)
   rspec-retry (~> 0.4.5)
   rspec-set (~> 0.1.3)
diff --git a/doc/development/testing.md b/doc/development/testing.md
index 3d5aa3d45e9f8..56dc8abd38a54 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -268,6 +268,43 @@ end
 - Avoid scenario titles that add no information, such as "successfully".
 - Avoid scenario titles that repeat the feature title.
 
+### Table-based / Parameterized tests
+
+This style of testing is used to exercise one piece of code with a comprehensive
+range of inputs. By specifying the test case once, alongside a table of inputs
+and the expected output for each, your tests can be made easier to read and more
+compact.
+
+We use the [rspec-parameterized](https://github.com/tomykaira/rspec-parameterized)
+gem. A short example, using the table syntax and checking Ruby equality for a
+range of inputs, might look like this:
+
+```ruby
+describe "#==" do
+  using Rspec::Parameterized::TableSyntax
+
+  let(:project1) { create(:project) }
+  let(:project2) { create(:project) }
+  where(:a, :b, :result) do
+    1         | 1        | true
+    1         | 2        | false
+    true      | true     | true
+    true      | false    | false
+    project1  | project1 | true
+    project2  | project2 | true
+    project 1 | project2 | false
+  end
+
+  with_them do
+    it { expect(a == b).to eq(result) }
+
+    it 'is isomorphic' do
+      expect(b == a).to eq(result)
+    end
+  end
+end
+```
+
 ### Matchers
 
 Custom matchers should be created to clarify the intent and/or hide the
diff --git a/spec/ee/spec/models/ee/issue_spec.rb b/spec/ee/spec/models/ee/issue_spec.rb
index fb1ac52d53b85..8c942b03939d2 100644
--- a/spec/ee/spec/models/ee/issue_spec.rb
+++ b/spec/ee/spec/models/ee/issue_spec.rb
@@ -1,6 +1,8 @@
 require 'spec_helper'
 
 describe Issue do
+  using RSpec::Parameterized::TableSyntax
+
   describe '#allows_multiple_assignees?' do
     it 'does not allow multiple assignees without license' do
       stub_licensed_features(multiple_issue_assignees: false)
@@ -20,24 +22,23 @@
   end
 
   describe '#weight' do
-    [
-      { license: true,  database: 5,    expected: 5 },
-      { license: true,  database: nil,  expected: nil },
-      { license: false, database: 5,    expected: nil },
-      { license: false, database: nil,  expected: nil }
-    ].each do |spec|
-      context spec.inspect do
-        let(:spec) { spec }
-        let(:issue) { build(:issue, weight: spec[:database]) }
-
-        subject { issue.weight }
-
-        before do
-          stub_licensed_features(issue_weights: spec[:license])
-        end
-
-        it { is_expected.to eq(spec[:expected]) }
+    where(:license_value, :database_value, :expected) do
+      true  | 5   | 5
+      true  | nil | nil
+      false | 5   | nil
+      false | nil | nil
+    end
+
+    with_them do
+      let(:issue) { build(:issue, weight: database_value) }
+
+      subject { issue.weight }
+
+      before do
+        stub_licensed_features(issue_weights: license_value)
       end
+
+      it { is_expected.to eq(expected) }
     end
   end
 end
diff --git a/spec/ee/spec/models/ee/merge_request_spec.rb b/spec/ee/spec/models/ee/merge_request_spec.rb
index c189ab42923c3..9a221fa660720 100644
--- a/spec/ee/spec/models/ee/merge_request_spec.rb
+++ b/spec/ee/spec/models/ee/merge_request_spec.rb
@@ -1,6 +1,8 @@
 require 'spec_helper'
 
 describe MergeRequest do
+  using RSpec::Parameterized::TableSyntax
+
   let(:project) { create(:project, :repository) }
 
   subject(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
@@ -135,24 +137,23 @@
   end
 
   describe '#approvals_before_merge' do
-    [
-      { license: true,  database: 5,   expected: 5 },
-      { license: true,  database: nil, expected: nil },
-      { license: false, database: 5,   expected: nil },
-      { license: false, database: nil, expected: nil }
-    ].each do |spec|
-      context spec.inspect do
-        let(:spec) { spec }
-        let(:merge_request) { build(:merge_request, approvals_before_merge: spec[:database]) }
-
-        subject { merge_request.approvals_before_merge }
-
-        before do
-          stub_licensed_features(merge_request_approvers: spec[:license])
-        end
-
-        it { is_expected.to eq(spec[:expected]) }
+    where(:license_value, :db_value, :expected) do
+      true  | 5   | 5
+      true  | nil | nil
+      false | 5   | nil
+      false | nil | nil
+    end
+
+    with_them do
+      let(:merge_request) { build(:merge_request, approvals_before_merge: db_value) }
+
+      subject { merge_request.approvals_before_merge }
+
+      before do
+        stub_licensed_features(merge_request_approvers: license_value)
       end
+
+      it { is_expected.to eq(expected) }
     end
   end
 end
diff --git a/spec/ee/spec/models/ee/project_import_data_spec.rb b/spec/ee/spec/models/ee/project_import_data_spec.rb
index ab5053c9b5024..51cbffe5a56cc 100644
--- a/spec/ee/spec/models/ee/project_import_data_spec.rb
+++ b/spec/ee/spec/models/ee/project_import_data_spec.rb
@@ -1,6 +1,8 @@
 require 'spec_helper'
 
 describe ProjectImportData do
+  using RSpec::Parameterized::TableSyntax
+
   let(:import_url) { 'ssh://example.com' }
   let(:import_data_attrs) { { auth_method: 'ssh_public_key' } }
   let(:project) { build(:project, :mirror, import_url: import_url, import_data_attributes: import_data_attrs) }
@@ -12,20 +14,19 @@
   end
 
   describe '#ssh_key_auth?' do
-    subject { import_data.ssh_key_auth? }
-
-    [
-      { import_url: 'ssh://example.com',  auth_method: 'ssh_public_key', expected: true  },
-      { import_url: 'ssh://example.com',  auth_method: 'password',       expected: false },
-      { import_url: 'http://example.com', auth_method: 'ssh_public_key', expected: false },
-      { import_url: 'http://example.com', auth_method: 'password',       expected: false }
-    ].each do |spec|
-      context spec.inspect do
-        let(:import_url) { spec[:import_url] }
-        let(:import_data_attrs) { { auth_method: spec[:auth_method] } }
-
-        it { is_expected.to spec[:expected] ? be_truthy : be_falsy }
-      end
+    where(:import_url, :auth_method, :expected) do
+      'ssh://example.com'  | 'ssh_public_key' | true
+      'ssh://example.com'  | 'password'       | false
+      'http://example.com' | 'ssh_public_key' | false
+      'http://example.com' | 'password'       | false
+    end
+
+    with_them do
+      let(:import_data_attrs) { { auth_method: auth_method } }
+
+      subject { import_data.ssh_key_auth? }
+
+      it { is_expected.to eq(expected) }
     end
   end
 
@@ -62,20 +63,18 @@
   end
 
   describe '#ssh_import?' do
-    subject { import_data.ssh_import? }
-
-    [
-      { import_url: nil,                   expected: false },
-      { import_url: 'ssh://example.com',   expected: true  },
-      { import_url: 'git://example.com',   expected: false },
-      { import_url: 'http://example.com',  expected: false },
-      { import_url: 'https://example.com', expected: false }
-    ].each do |spec|
-      context spec.inspect do
-        let(:import_url) { spec[:import_url] }
-
-        it { is_expected.to spec[:expected] ? be_truthy : be_falsy }
-      end
+    where(:import_url, :expected) do
+      'ssh://example.com'   | true
+      'git://example.com'   | false
+      'http://example.com'  | false
+      'https://example.com' | false
+      nil                   | nil
+    end
+
+    with_them do
+      subject { import_data.ssh_import? }
+
+      it { is_expected.to eq(expected) }
     end
   end
 
diff --git a/spec/ee/spec/models/ee/project_spec.rb b/spec/ee/spec/models/ee/project_spec.rb
index 515c7bc816e1c..b88d1b1d337e8 100644
--- a/spec/ee/spec/models/ee/project_spec.rb
+++ b/spec/ee/spec/models/ee/project_spec.rb
@@ -1,6 +1,8 @@
 require 'spec_helper'
 
 describe Project do
+  using RSpec::Parameterized::TableSyntax
+
   describe 'associations' do
     it { is_expected.to delegate_method(:shared_runners_minutes).to(:statistics) }
     it { is_expected.to delegate_method(:shared_runners_seconds).to(:statistics) }
@@ -639,102 +641,98 @@
   end
 
   describe '#approvals_before_merge' do
-    [
-      { license: true,  database: 5,  expected: 5 },
-      { license: true,  database: 0,  expected: 0 },
-      { license: false, database: 5,  expected: 0 },
-      { license: false, database: 0,  expected: 0 }
-    ].each do |spec|
-      context spec.inspect do
-        let(:spec) { spec }
-        let(:project) { build(:project, approvals_before_merge: spec[:database]) }
-
-        subject { project.approvals_before_merge }
+    where(:license_value, :db_value, :expected) do
+      true  | 5 | 5
+      true  | 0 | 0
+      false | 5 | 0
+      false | 0 | 0
+    end
 
-        before do
-          stub_licensed_features(merge_request_approvers: spec[:license])
-        end
+    with_them do
+      let(:project) { build(:project, approvals_before_merge: db_value) }
+
+      subject { project.approvals_before_merge }
 
-        it { is_expected.to eq(spec[:expected]) }
+      before do
+        stub_licensed_features(merge_request_approvers: license_value)
       end
+
+      it { is_expected.to eq(expected) }
     end
   end
 
   describe "#reset_approvals_on_push?" do
-    [
-      { license: true,  database: true,  expected: true },
-      { license: true,  database: false, expected: false },
-      { license: false, database: true,  expected: false },
-      { license: false, database: false, expected: false }
-    ].each do |spec|
-      context spec.inspect do
-        let(:spec) { spec }
-        let(:project) { build(:project, reset_approvals_on_push: spec[:database]) }
-
-        subject { project.reset_approvals_on_push? }
+    where(:license_value, :db_value, :expected) do
+      true  | true  | true
+      true  | false | false
+      false | true  | false
+      false | false | false
+    end
 
-        before do
-          stub_licensed_features(merge_request_approvers: spec[:license])
-        end
+    with_them do
+      let(:project) { build(:project, reset_approvals_on_push: db_value) }
+
+      subject { project.reset_approvals_on_push? }
 
-        it { is_expected.to eq(spec[:expected]) }
+      before do
+        stub_licensed_features(merge_request_approvers: license_value)
       end
+
+      it { is_expected.to eq(expected) }
     end
   end
 
   describe '#approvals_before_merge' do
-    [
-      { license: true,  database: 5,  expected: 5 },
-      { license: true,  database: 0,  expected: 0 },
-      { license: false, database: 5,  expected: 0 },
-      { license: false, database: 0,  expected: 0 }
-    ].each do |spec|
-      context spec.inspect do
-        let(:spec) { spec }
-        let(:project) { build(:project, approvals_before_merge: spec[:database]) }
-
-        subject { project.approvals_before_merge }
+    where(:license_value, :db_value, :expected) do
+      true  | 5 | 5
+      true  | 0 | 0
+      false | 5 | 0
+      false | 0 | 0
+    end
 
-        before do
-          stub_licensed_features(merge_request_approvers: spec[:license])
-        end
+    with_them do
+      let(:project) { build(:project, approvals_before_merge: db_value) }
+
+      subject { project.approvals_before_merge }
 
-        it { is_expected.to eq(spec[:expected]) }
+      before do
+        stub_licensed_features(merge_request_approvers: license_value)
       end
+
+      it { is_expected.to eq(expected) }
     end
   end
 
   describe '#merge_method' do
-    [
-      { ff: true,  rebase: true,  ff_licensed: true,  rebase_licensed: true,  method: :ff },
-      { ff: true,  rebase: true,  ff_licensed: true,  rebase_licensed: false, method: :ff },
-      { ff: true,  rebase: true,  ff_licensed: false, rebase_licensed: true,  method: :rebase_merge },
-      { ff: true,  rebase: true,  ff_licensed: false, rebase_licensed: false, method: :merge },
-      { ff: true,  rebase: false, ff_licensed: true,  rebase_licensed: true,  method: :ff },
-      { ff: true,  rebase: false, ff_licensed: true,  rebase_licensed: false, method: :ff },
-      { ff: true,  rebase: false, ff_licensed: false, rebase_licensed: true,  method: :merge },
-      { ff: true,  rebase: false, ff_licensed: false, rebase_licensed: false, method: :merge },
-      { ff: false, rebase: true,  ff_licensed: true,  rebase_licensed: true,  method: :rebase_merge },
-      { ff: false, rebase: true,  ff_licensed: true,  rebase_licensed: false, method: :merge },
-      { ff: false, rebase: true,  ff_licensed: false, rebase_licensed: true,  method: :rebase_merge },
-      { ff: false, rebase: true,  ff_licensed: false, rebase_licensed: false, method: :merge },
-      { ff: false, rebase: false, ff_licensed: true,  rebase_licensed: true,  method: :merge },
-      { ff: false, rebase: false, ff_licensed: true,  rebase_licensed: false, method: :merge },
-      { ff: false, rebase: false, ff_licensed: false, rebase_licensed: true,  method: :merge },
-      { ff: false, rebase: false, ff_licensed: false, rebase_licensed: false, method: :merge }
-    ].each do |spec|
-      context spec.inspect do
-        let(:project) { build(:project, merge_requests_rebase_enabled: spec[:rebase], merge_requests_ff_only_enabled: spec[:ff]) }
-        let(:spec) { spec }
-
-        subject { project.merge_method }
-
-        before do
-          stub_licensed_features(merge_request_rebase: spec[:rebase_licensed], fast_forward_merge: spec[:ff_licensed])
-        end
+    where(:ff, :rebase, :ff_licensed, :rebase_licensed, :method) do
+      true  | true  | true  | true  | :ff
+      true  | true  | true  | false | :ff
+      true  | true  | false | true  | :rebase_merge
+      true  | true  | false | false | :merge
+      true  | false | true  | true  | :ff
+      true  | false | true  | false | :ff
+      true  | false | false | true  | :merge
+      true  | false | false | false | :merge
+      false | true  | true  | true  | :rebase_merge
+      false | true  | true  | false | :merge
+      false | true  | false | true  | :rebase_merge
+      false | true  | false | false | :merge
+      false | false | true  | true  | :merge
+      false | false | true  | false | :merge
+      false | false | false | true  | :merge
+      false | false | false | false | :merge
+    end
+
+    with_them do
+      let(:project) { build(:project, merge_requests_rebase_enabled: rebase, merge_requests_ff_only_enabled: ff) }
+
+      subject { project.merge_method }
 
-        it { is_expected.to eq(spec[:method]) }
+      before do
+        stub_licensed_features(merge_request_rebase: rebase_licensed, fast_forward_merge: ff_licensed)
       end
+
+      it { is_expected.to eq(method) }
     end
   end
 
diff --git a/spec/ee/spec/models/ssh_host_key_spec.rb b/spec/ee/spec/models/ssh_host_key_spec.rb
index e00660d4620fb..8bfda246af577 100644
--- a/spec/ee/spec/models/ssh_host_key_spec.rb
+++ b/spec/ee/spec/models/ssh_host_key_spec.rb
@@ -1,18 +1,37 @@
 require 'spec_helper'
 
 describe SshHostKey do
+  using RSpec::Parameterized::TableSyntax
   include ReactiveCachingHelpers
 
-  keys = [
-    SSHKeygen.generate,
-    SSHKeygen.generate
-  ]
+  let(:key1) do
+    'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3UpyF2iLqy1d63M6k3jH1vuEnq/NWtE+o' \
+    'rJe1Xn7JoRbduKd6zpsJ0JhBGWgcQK0ph0aGW5PcudzzBSc+SlYfCc4GTaxDtmj41hW0o72m' \
+    'NiuDW3oKXXShOiVRde2ZOquH8Z865jGiZIC8BI/bXZD29IGUih0hPu7Rjp70VYiE+35QRf/p' \
+    'sD0Ddrz8QUIG3A/2dMzLI5F5ZORk3BIX2F3mJwJOvZxRhR/SqyphDMZ5eZ0EzqbFBCDE6HAB' \
+    'Woz9ck8RBGLvCIggmDHj3FmMLcQGMDiy6wKp7QdnBtxjCP6vtE6YPUM223AqsWt+9NTtCfB8' \
+    'YdNAH7YcHHOR1FgtSk1x'
+  end
+
+  let(:key2) do
+    'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLIp+4ciR2YO9f9rpldc7InNQw/TBUtcNb' \
+    'J2XR0rr15/5ytz7YM16xXG0Qjx576PNSmqs4gbTrvTuFZak+v1Jx/9deHRq/yqp9f+tv33+i' \
+    'aJGCQCX/+OVY7aWgV2R9YsS7XQ4mnv4XlOTEssib/rGAIT+ATd/GcdYSEOO+dh4O09/6O/jI' \
+    'MGSeP+NNetgn1nPCnLOjrXFZUnUtNDi6EEKeIlrliJjSb7Jr4f7gjvZnv4RskWHHFo8FgAAq' \
+    't0gOMT6EmKrnypBe2vLGSAXbtkXr01q6/DNPH+n9VA1LTV6v1KN/W5CN5tQV11wRSKiM8g5O' \
+    'Ebi86VjJRi2sOuYoXQU1'
+  end
 
   # Purposefully ordered so that `sort` will make changes
-  known_hosts = <<~EOF
-    example.com #{keys[0]} git@localhost
-    @revoked other.example.com #{keys[1]} git@localhost
-  EOF
+  let(:known_hosts) do
+    <<~EOF
+      example.com #{key1} git@localhost
+      @revoked other.example.com #{key2} git@localhost
+    EOF
+  end
+
+  let(:extra) { known_hosts + "foo\nbar\n" }
+  let(:reversed) { known_hosts.lines.reverse.join }
 
   def stub_ssh_keyscan(args, status: true, stdout: "", stderr: "")
     stdin = StringIO.new
@@ -33,7 +52,7 @@ def stub_ssh_keyscan(args, status: true, stdout: "", stderr: "")
     it 'returns an array of indexed fingerprints when the cache is filled' do
       stub_reactive_cache(ssh_host_key, known_hosts: known_hosts)
 
-      expected = keys
+      expected = [key1, key2]
         .map { |data| Gitlab::KeyFingerprint.new(data) }
         .each_with_index
         .map { |key, i| { bits: key.bits, fingerprint: key.fingerprint, type: key.type, index: i } } 
@@ -48,16 +67,12 @@ def stub_ssh_keyscan(args, status: true, stdout: "", stderr: "")
 
   describe '#fingerprints', use_clean_rails_memory_store_caching: true do
     it 'returns an array of indexed fingerprints when the cache is filled' do
-      key1 = SSHKeygen.generate
-      key2 = SSHKeygen.generate
-
-      known_hosts = "example.com #{key1} git@localhost\n\n\n@revoked other.example.com #{key2} git@localhost\n"
       stub_reactive_cache(ssh_host_key, known_hosts: known_hosts)
 
       expect(ssh_host_key.fingerprints.as_json).to eq(
         [
           { bits: 2048, fingerprint: Gitlab::KeyFingerprint.new(key1).fingerprint, type: 'RSA', index: 0 },
-          { bits: 2048, fingerprint: Gitlab::KeyFingerprint.new(key2).fingerprint, type: 'RSA', index: 3 }
+          { bits: 2048, fingerprint: Gitlab::KeyFingerprint.new(key2).fingerprint, type: 'RSA', index: 1 }
         ]
       )
     end
@@ -68,36 +83,35 @@ def stub_ssh_keyscan(args, status: true, stdout: "", stderr: "")
   end
 
   describe '#changes_project_import_data?' do
-    subject { ssh_host_key.changes_project_import_data? }
-
-    reversed = known_hosts.lines.reverse.join
-    extra = known_hosts + "foo\nbar\n"
-
-    [
-      { a: known_hosts, b: extra,       result: true  },
-      { a: known_hosts, b: "foo\n",     result: true  },
-      { a: known_hosts, b: '',          result: true  },
-      { a: known_hosts, b: nil,         result: true  },
-      { a: known_hosts, b: known_hosts, result: false },
-      { a: reversed,    b: known_hosts, result: false },
-      { a: extra,       b: "foo\n",     result: true  },
-      { a: '',          b: '',          result: false },
-      { a: nil,         b: nil,         result: false },
-      { a: '',          b: nil,         result: false }
-    ].each_with_index do |spec, index|
-      it "is #{spec[:result]} for test case #{index}" do
-        expect(ssh_host_key).to receive(:known_hosts).and_return(spec[:a])
-        project.import_data.ssh_known_hosts = spec[:b]
-
-        is_expected.to eq(spec[:result])
+    where(:a, :b, :result) do
+      known_hosts | extra       | true
+      known_hosts | "foo\n"     | true
+      known_hosts | ''          | true
+      known_hosts | nil         | true
+      known_hosts | known_hosts | false
+      reversed    | known_hosts | false
+      extra       | "foo\n"     | true
+      ''          | ''          | false
+      nil         | nil         | false
+      ''          | nil         | false
+    end
+
+    with_them do
+      subject { ssh_host_key.changes_project_import_data? }
+
+      it "(normal)" do
+        expect(ssh_host_key).to receive(:known_hosts).and_return(a)
+        project.import_data.ssh_known_hosts = b
+
+        is_expected.to eq(result)
       end
 
       # Comparisons should be symmetrical, so test the reverse too
-      it "is #{spec[:result]} for test case #{index} (reversed)" do
-        expect(ssh_host_key).to receive(:known_hosts).and_return(spec[:b])
-        project.import_data.ssh_known_hosts = spec[:a]
+      it "(reversed)" do
+        expect(ssh_host_key).to receive(:known_hosts).and_return(b)
+        project.import_data.ssh_known_hosts = a
 
-        is_expected.to eq(spec[:result])
+        is_expected.to eq(result)
       end
     end
   end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index a827f658fd507..5698842279941 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -8,6 +8,7 @@
 require 'rspec/rails'
 require 'shoulda/matchers'
 require 'rspec/retry'
+require 'rspec-parameterized'
 
 rspec_profiling_is_configured =
   ENV['RSPEC_PROFILING_POSTGRES_URL'].present? ||
-- 
GitLab