diff --git a/Gemfile b/Gemfile
index d7c1c3576f08d4dbe174b83540956ab1190a19db..8e2fd0633b8801f2b9bdc2c8821186731920d781 100644
--- a/Gemfile
+++ b/Gemfile
@@ -394,7 +394,7 @@ group :development, :test do
 end
 
 group :test do
-  gem 'shoulda-matchers', '~> 3.1.2', require: false
+  gem 'shoulda-matchers', '~> 4.0.1', require: false
   gem 'email_spec', '~> 2.2.0'
   gem 'json-schema', '~> 2.8.0'
   gem 'webmock', '~> 3.5.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 9d36470028a3c1622b85548e1694f6234f5e6d8a..de54d5fc8d037a3ddd5a4534fc30931e4bddd2ef 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -888,8 +888,8 @@ GEM
     sexp_processor (4.12.0)
     sham_rack (1.3.6)
       rack
-    shoulda-matchers (3.1.2)
-      activesupport (>= 4.0.0)
+    shoulda-matchers (4.0.1)
+      activesupport (>= 4.2.0)
     sidekiq (5.2.7)
       connection_pool (~> 2.2, >= 2.2.2)
       rack (>= 1.5.0)
@@ -1241,7 +1241,7 @@ DEPENDENCIES
   sentry-raven (~> 2.7)
   settingslogic (~> 2.0.9)
   sham_rack (~> 1.3.6)
-  shoulda-matchers (~> 3.1.2)
+  shoulda-matchers (~> 4.0.1)
   sidekiq (~> 5.2.7)
   sidekiq-cron (~> 1.0)
   simple_po_parser (~> 1.1.2)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 0e6ad26bc1c2ad79bbff789a2d31549414a9b26b..6168345047d8ace59b36630311141d7d49a5e9d7 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -583,6 +583,8 @@ def branch_merge_base_sha
   end
 
   def validate_branches
+    return unless target_project && source_project
+
     if target_project == source_project && target_branch == source_branch
       errors.add :branch_conflict, "You can't use same project/branch for source and target"
       return
diff --git a/ee/app/models/ee/board.rb b/ee/app/models/ee/board.rb
index c00ff823cde76de0247b0ff88986697526c96b54..162c10a74dfbfa6127f6466059584a1c6614f2d5 100644
--- a/ee/app/models/ee/board.rb
+++ b/ee/app/models/ee/board.rb
@@ -35,7 +35,7 @@ def scoped?
     end
 
     def milestone
-      return unless parent.feature_available?(:scoped_issue_board)
+      return unless parent&.feature_available?(:scoped_issue_board)
 
       case milestone_id
       when ::Milestone::Upcoming.id
diff --git a/ee/spec/models/group_spec.rb b/ee/spec/models/group_spec.rb
index b22abd5e55d67242c0895b019f7d330ad1a5d6aa..6bb94b5d3121a38969fbbee6a02418462196799d 100644
--- a/ee/spec/models/group_spec.rb
+++ b/ee/spec/models/group_spec.rb
@@ -7,7 +7,10 @@
 
   describe 'associations' do
     it { is_expected.to have_many(:audit_events).dependent(false) }
-    it { is_expected.to belong_to(:file_template_project) }
+    # shoulda-matchers attempts to set the association to nil to ensure
+    # the presence check works, but since this is a private method that
+    # method can't be called with a public_send.
+    it { is_expected.to belong_to(:file_template_project).class_name('Project').without_validating_presence }
     it { is_expected.to have_many(:dependency_proxy_blobs) }
     it { is_expected.to have_one(:dependency_proxy_setting) }
   end
diff --git a/ee/spec/models/prometheus_alert_event_spec.rb b/ee/spec/models/prometheus_alert_event_spec.rb
index a56525d2b722c0a12ee49734ab908e6de8e5b20b..d867da41a27e2aba51ca8a50e60ceddfd4e02eed 100644
--- a/ee/spec/models/prometheus_alert_event_spec.rb
+++ b/ee/spec/models/prometheus_alert_event_spec.rb
@@ -7,7 +7,7 @@
   let(:alert) { subject.prometheus_alert }
 
   describe 'associations' do
-    it { is_expected.to belong_to(:prometheus_alert) }
+    it { is_expected.to belong_to(:prometheus_alert).required }
   end
 
   describe 'validations' do
diff --git a/ee/spec/models/prometheus_alert_spec.rb b/ee/spec/models/prometheus_alert_spec.rb
index cfc48e3639cfcec493e732502b24262af9450325..34dc325e5e2271e3fe159833554b8a69e400e13e 100644
--- a/ee/spec/models/prometheus_alert_spec.rb
+++ b/ee/spec/models/prometheus_alert_spec.rb
@@ -24,8 +24,8 @@
   end
 
   describe 'associations' do
-    it { is_expected.to belong_to(:project) }
-    it { is_expected.to belong_to(:environment) }
+    it { is_expected.to belong_to(:project).required }
+    it { is_expected.to belong_to(:environment).required }
   end
 
   describe 'project validations' do
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index 1bfc14d2839de1218bec19edc07f4256f0950145..42d4769a921e858e56787cb4115794d202b860d3 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -3,6 +3,8 @@
 require 'spec_helper'
 
 describe Ci::PipelineSchedule do
+  subject { build(:ci_pipeline_schedule) }
+
   it { is_expected.to belong_to(:project) }
   it { is_expected.to belong_to(:owner) }
 
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 58203da5b224bd974ffd210244c6ac15f3e1a297..f9d8ffd06e0d23eda81e1b9792228f3aa27fad84 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -5,6 +5,8 @@
 describe Clusters::Cluster do
   it_behaves_like 'having unique enum values'
 
+  subject { build(:cluster) }
+
   it { is_expected.to belong_to(:user) }
   it { is_expected.to have_many(:cluster_projects) }
   it { is_expected.to have_many(:projects) }
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index f51322e14040b0b7e41ea542dcba8de71bf77479..1dceef3fc006578fd8179960e479a7cd46f15cf1 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -5,8 +5,8 @@
 describe Deployment do
   subject { build(:deployment) }
 
-  it { is_expected.to belong_to(:project) }
-  it { is_expected.to belong_to(:environment) }
+  it { is_expected.to belong_to(:project).required }
+  it { is_expected.to belong_to(:environment).required }
   it { is_expected.to belong_to(:user) }
   it { is_expected.to belong_to(:deployable) }
 
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 17246f238e0a525f0be6429b930d895d8bf6ba4b..7233d2454c62750f209e1040793d65ed7037edc9 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -6,7 +6,7 @@
   let(:project) { create(:project, :stubbed_repository) }
   subject(:environment) { create(:environment, project: project) }
 
-  it { is_expected.to belong_to(:project) }
+  it { is_expected.to belong_to(:project).required }
   it { is_expected.to have_many(:deployments) }
 
   it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
diff --git a/spec/support/shoulda/matchers/rails_shim.rb b/spec/support/shoulda/matchers/rails_shim.rb
deleted file mode 100644
index 8d70598beb568fd8d5914a1a7746ed0399c0aedc..0000000000000000000000000000000000000000
--- a/spec/support/shoulda/matchers/rails_shim.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# monkey patch which fixes serialization matcher in Rails 5
-# https://github.com/thoughtbot/shoulda-matchers/issues/913
-# This can be removed when a new version of shoulda-matchers
-# is released
-module Shoulda
-  module Matchers
-    class RailsShim
-      def self.serialized_attributes_for(model)
-        if defined?(::ActiveRecord::Type::Serialized)
-          # Rails 5+
-          serialized_columns = model.columns.select do |column|
-            model.type_for_attribute(column.name).is_a?(
-              ::ActiveRecord::Type::Serialized
-            )
-          end
-
-          serialized_columns.inject({}) do |hash, column| # rubocop:disable Style/EachWithObject
-            hash[column.name.to_s] = model.type_for_attribute(column.name).coder
-            hash
-          end
-        else
-          model.serialized_attributes
-        end
-      end
-    end
-  end
-end