diff --git a/.gitignore b/.gitignore
index 0e46bc4..3f0816c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@
 /tmp/
 /spec/fixtures/modules/augeas_core/
 /spec/fixtures/modules/stdlib/
+/coverage/
diff --git a/.travis.yml b/.travis.yml
index bc9eb28..15c8c2a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,8 +14,9 @@ sudo: false
 env:
   - PUPPET_GEM_VERSION='~> 6.0' COVERAGE=yes
   - PUPPET_GEM_VERSION='~> 6.0' FACTER_GEM_VERSION='3.11.2.cfacter.20180612'
+  - PUPPET_GEM_VERSION='~> 7.0' COVERAGE=yes
   # Latest code from puppetlabs/puppet.git
-  - PUPPET_GEM_VERSION='https://github.com/puppetlabs/puppet.git#master'
+  - PUPPET_GEM_VERSION='https://github.com/puppetlabs/puppet.git#main'
 
 matrix:
   include:
@@ -35,7 +36,7 @@ matrix:
 
   allowed_failures:
     # Don't fail for puppet.git#master because it may be to blame for any failures
-    - env: PUPPET_GEM_VERSION='https://github.com/puppetlabs/puppet.git#master'
+    - env: PUPPET_GEM_VERSION='https://github.com/puppetlabs/puppet.git#main'
     # Don't fail for very old versions of Ruby/Puppet at this point
     - rvm: 1.8.7
     - rvm: 1.9.3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 37d2494..54533b3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
 All notable changes to this project will be documented in this file. This
 project adheres to [Semantic Versioning](http://semver.org/).
 
+## [2.9.0]
+
+### Added
+ * Allow users to disable app_management for Puppet 4
+ * Added support for regexp arguments to Sensitive
+ * Handle all auto*, not just autorequire
+ * Set up loaders so that 4.x functions resolve properly
+
 ## [2.8.0]
 
 ### Breaking Changes
@@ -546,7 +554,36 @@ Thanks to Adrien Thebo, Alex Harvey, Brian, Dan Bode, Dominic Cleal, Javier Pala
 ## 1.0.1 and earlier
 For changelog of versions 1.0.1 and earlier, see http://rspec-puppet.com/changelog/
 
-[2.x]: https://github.com/rodjek/rspec-puppet/compare/v2.5.0...master
+[2.x]: https://github.com/rodjek/rspec-puppet/compare/v2.9.0...master
+[2.9.0]: https://github.com/rodjek/rspec-puppet/compare/v2.8.0...v2.9.0
+[2.8.0]: https://github.com/rodjek/rspec-puppet/compare/v2.7.10...v2.8.0
+[2.7.10]: https://github.com/rodjek/rspec-puppet/compare/v2.7.9...v2.7.10
+[2.7.9]: https://github.com/rodjek/rspec-puppet/compare/v2.7.8...v2.7.9
+[2.7.8]: https://github.com/rodjek/rspec-puppet/compare/v2.7.7...v2.7.8
+[2.7.7]: https://github.com/rodjek/rspec-puppet/compare/v2.7.6...v2.7.7
+[2.7.6]: https://github.com/rodjek/rspec-puppet/compare/v2.7.5...v2.7.6
+[2.7.5]: https://github.com/rodjek/rspec-puppet/compare/v2.7.4...v2.7.5
+[2.7.4]: https://github.com/rodjek/rspec-puppet/compare/v2.7.3...v2.7.4
+[2.7.3]: https://github.com/rodjek/rspec-puppet/compare/v2.7.2...v2.7.3
+[2.7.2]: https://github.com/rodjek/rspec-puppet/compare/v2.7.1...v2.7.2
+[2.7.1]: https://github.com/rodjek/rspec-puppet/compare/v2.7.0...v2.7.1
+[2.7.0]: https://github.com/rodjek/rspec-puppet/compare/v2.6.15...v2.7.0
+[2.6.15]: https://github.com/rodjek/rspec-puppet/compare/v2.6.14...v2.6.15
+[2.6.14]: https://github.com/rodjek/rspec-puppet/compare/v2.6.13...v2.6.14
+[2.6.13]: https://github.com/rodjek/rspec-puppet/compare/v2.6.12...v2.6.13
+[2.6.12]: https://github.com/rodjek/rspec-puppet/compare/v2.6.11...v2.6.12
+[2.6.11]: https://github.com/rodjek/rspec-puppet/compare/v2.6.10...v2.6.11
+[2.6.10]: https://github.com/rodjek/rspec-puppet/compare/v2.6.9...v2.6.10
+[2.6.9]: https://github.com/rodjek/rspec-puppet/compare/v2.6.8...v2.6.9
+[2.6.8]: https://github.com/rodjek/rspec-puppet/compare/v2.6.7...v2.6.8
+[2.6.7]: https://github.com/rodjek/rspec-puppet/compare/v2.6.6...v2.6.7
+[2.6.6]: https://github.com/rodjek/rspec-puppet/compare/v2.6.5...v2.6.6
+[2.6.5]: https://github.com/rodjek/rspec-puppet/compare/v2.6.4...v2.6.5
+[2.6.4]: https://github.com/rodjek/rspec-puppet/compare/v2.6.3...v2.6.4
+[2.6.3]: https://github.com/rodjek/rspec-puppet/compare/v2.6.2...v2.6.3
+[2.6.2]: https://github.com/rodjek/rspec-puppet/compare/v2.6.1...v2.6.2
+[2.6.1]: https://github.com/rodjek/rspec-puppet/compare/v2.6.0...v2.6.1
+[2.6.0]: https://github.com/rodjek/rspec-puppet/compare/2.5.0...v2.6.0
 [2.5.0]: https://github.com/rodjek/rspec-puppet/compare/v2.4.0...v2.5.0
 [2.4.0]: https://github.com/rodjek/rspec-puppet/compare/v2.3.2...v2.4.0
 [2.3.2]: https://github.com/rodjek/rspec-puppet/compare/v2.3.1...v2.3.2
diff --git a/Gemfile b/Gemfile
index 990668b..99e9d0d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -43,8 +43,9 @@ else
   gem 'sync' if (RUBY_VERSION >= '2.7.0')
 end
 
-if ENV['COVERAGE'] == 'yes'
+if ENV['COVERAGE']
   gem 'coveralls', :require => false
+  gem 'simplecov', :require => false
 end
 
 gem 'win32-taskscheduler', :platforms => [:mingw, :x64_mingw, :mswin]
diff --git a/Rakefile b/Rakefile
index 26c346e..6edf9c1 100644
--- a/Rakefile
+++ b/Rakefile
@@ -35,6 +35,10 @@ namespace :test do
     end
   end
 
+  RSpec::Core::RakeTask.new(:spec_unit) do |t|
+    t.pattern = 'spec/unit/**/*_spec.rb'
+  end
+
   task :setup do
     puppet_version = Gem::Version.new(Puppet.version)
 
@@ -64,6 +68,17 @@ namespace :test do
       end
     end
   end
+
+  task :unit do
+    begin
+      Rake::Task['test:setup'].invoke
+      ENV['COVERAGE'] = 'local'
+      Rake::Task['test:spec_unit'].invoke
+    ensure
+      ENV.delete('COVERAGE')
+      Rake::Task['test:teardown'].invoke
+    end
+  end
 end
 
 task :test do
diff --git a/appveyor.yml b/appveyor.yml
index 4e5c1a7..5278e0e 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -7,26 +7,25 @@ branches:
 # ruby versions under test
 environment:
   matrix:
-    - RUBY_VERSION: 21
-      PUPPET_GEM_VERSION: '~> 4.0'
-    - RUBY_VERSION: 23-x64
-      PUPPET_GEM_VERSION: '~> 5.0'
     - RUBY_VERSION: 25-x64
       PUPPET_GEM_VERSION: '~> 6.0'
     - RUBY_VERSION: 25-x64
       PUPPET_GEM_VERSION: '~> 6.0'
       FACTER_GEM_VERSION: '3.11.2.cfacter.20180612'
+    - APPVEYOR_BUILD_WORKER_IMAGE: 'Visual Studio 2019'
+      RUBY_VERSION: 27-x64
+      PUPPET_GEM_VERSION: '~> 7.0'
     # Latest code from puppetlabs/puppet.git
     - RUBY_VERSION: 25-x64
-      PUPPET_GEM_VERSION: 'https://github.com/puppetlabs/puppet.git#master'
+      PUPPET_GEM_VERSION: 'https://github.com/puppetlabs/puppet.git#main'
     - APPVEYOR_BUILD_WORKER_IMAGE: 'Visual Studio 2019'
       RUBY_VERSION: 27-x64
-      PUPPET_GEM_VERSION: 'https://github.com/puppetlabs/puppet.git#master'
+      PUPPET_GEM_VERSION: 'https://github.com/puppetlabs/puppet.git#main'
 
 matrix:
   allow_failures:
     # Don't fail for puppet.git#master because it may be to blame for any failures
-    - PUPPET_GEM_VERSION: 'https://github.com/puppetlabs/puppet.git#master'
+    - PUPPET_GEM_VERSION: 'https://github.com/puppetlabs/puppet.git#main'
 
 install:
   - SET PATH=C:\Ruby%RUBY_VERSION%\bin;C:\Ruby%RUBY_VERSION%\lib\ruby\gems\2.5.0\gems\facter-3.11.2.cfacter.20180612-x64-mingw32\bin;%PATH%
diff --git a/debian/changelog b/debian/changelog
index 60387a5..2cefba3 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+ruby-rspec-puppet (2.9.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Thu, 10 Mar 2022 15:22:02 -0000
+
 ruby-rspec-puppet (2.8.0-2) unstable; urgency=medium
 
   * Team upload.
diff --git a/debian/patches/001_disable-failing-upstream-tests.patch b/debian/patches/001_disable-failing-upstream-tests.patch
index 663c531..442e72b 100644
--- a/debian/patches/001_disable-failing-upstream-tests.patch
+++ b/debian/patches/001_disable-failing-upstream-tests.patch
@@ -3,8 +3,10 @@ Date: Tue, 4 Feb 2020 19:12:02 +0100
 Subject: Disable failing upstream tests
 Forwarded: not-needed
 Last-Update: 2020-02-04
---- a/spec/classes/test_registry_spec.rb
-+++ b/spec/classes/test_registry_spec.rb
+Index: ruby-rspec-puppet/spec/classes/test_registry_spec.rb
+===================================================================
+--- ruby-rspec-puppet.orig/spec/classes/test_registry_spec.rb
++++ ruby-rspec-puppet/spec/classes/test_registry_spec.rb
 @@ -7,5 +7,5 @@ describe 'test::registry', :if => Puppet
      }
    end
@@ -12,8 +14,10 @@ Last-Update: 2020-02-04
 -  it { should compile.with_all_deps }
 +  xit { should compile.with_all_deps }
  end
---- a/spec/functions/ensure_packages_spec.rb
-+++ b/spec/functions/ensure_packages_spec.rb
+Index: ruby-rspec-puppet/spec/functions/ensure_packages_spec.rb
+===================================================================
+--- ruby-rspec-puppet.orig/spec/functions/ensure_packages_spec.rb
++++ ruby-rspec-puppet/spec/functions/ensure_packages_spec.rb
 @@ -3,7 +3,7 @@ require 'spec_helper'
  describe 'ensure_packages' do
    before { subject.execute('facter') }
diff --git a/debian/patches/Add-Ruby-3.0-support.patch b/debian/patches/Add-Ruby-3.0-support.patch
index 8727d2c..6be6549 100644
--- a/debian/patches/Add-Ruby-3.0-support.patch
+++ b/debian/patches/Add-Ruby-3.0-support.patch
@@ -10,10 +10,10 @@ Bug-Debian: https://bugs.debian.org/998509
  lib/rspec-puppet/support.rb | 2 +-
  1 file changed, 1 insertion(+), 1 deletion(-)
 
-diff --git a/lib/rspec-puppet/support.rb b/lib/rspec-puppet/support.rb
-index f5e7d39..420297b 100644
---- a/lib/rspec-puppet/support.rb
-+++ b/lib/rspec-puppet/support.rb
+Index: ruby-rspec-puppet/lib/rspec-puppet/support.rb
+===================================================================
+--- ruby-rspec-puppet.orig/lib/rspec-puppet/support.rb
++++ ruby-rspec-puppet/lib/rspec-puppet/support.rb
 @@ -485,7 +485,7 @@ module RSpec::Puppet
      def build_catalog(*args)
        @@cache.get(*args) do |*args|
diff --git a/lib/rspec-puppet/adapters.rb b/lib/rspec-puppet/adapters.rb
index 042882a..c4a4675 100644
--- a/lib/rspec-puppet/adapters.rb
+++ b/lib/rspec-puppet/adapters.rb
@@ -139,7 +139,8 @@ module RSpec::Puppet
         Puppet.push_context(
           {
             :environments => loader,
-            :current_environment => env
+            :current_environment => env,
+            :loaders => (Puppet::Pops::Loaders.new(env) if Gem::Version.new(Puppet.version) >= Gem::Version.new('6.0.0')),
           },
           "Setup rspec-puppet environments"
         )
diff --git a/lib/rspec-puppet/matchers/count_generic.rb b/lib/rspec-puppet/matchers/count_generic.rb
index 6aa04a1..28d6efe 100644
--- a/lib/rspec-puppet/matchers/count_generic.rb
+++ b/lib/rspec-puppet/matchers/count_generic.rb
@@ -7,13 +7,15 @@ module RSpec::Puppet
         'Stage[main]',
       ].freeze
 
+      attr_reader :resource_type
+
       def initialize(type, count, *method)
         if type.nil?
           @type = method[0].to_s.gsub(/^have_(.+)_resource_count$/, '\1')
         else
           @type = type
         end
-        @referenced_type = referenced_type(@type)
+        @resource_type = referenced_type(@type)
         @expected_number = count.to_i
       end
 
@@ -30,7 +32,7 @@ module RSpec::Puppet
                            end
                          else
                            resources.count do |res|
-                             res.type == @referenced_type
+                             res.type == @resource_type
                            end
                          end
 
@@ -45,7 +47,7 @@ module RSpec::Puppet
           desc << "#{@expected_number == 1 ? "class" : "classes" }"
         else
           unless @type == "resource"
-            desc << "#{@referenced_type}"
+            desc << "#{@resource_type}"
           end
           desc << "#{@expected_number == 1 ? "resource" : "resources" }"
         end
diff --git a/lib/rspec-puppet/matchers/create_generic.rb b/lib/rspec-puppet/matchers/create_generic.rb
index 0c7b07d..971590e 100644
--- a/lib/rspec-puppet/matchers/create_generic.rb
+++ b/lib/rspec-puppet/matchers/create_generic.rb
@@ -307,14 +307,17 @@ module RSpec::Puppet
           end
         end
 
-        # Add autorequires if any
-        if type == :require and resource.resource_type.respond_to? :eachautorequire
-          resource.resource_type.eachautorequire do |t, b|
-            Array(resource.to_ral.instance_eval(&b)).each do |dep|
-              res = "#{t.to_s.capitalize}[#{dep}]"
-              if r = relationship_refs(res, type, visited)
-                results << res
-                results << r
+        # Add auto* (autorequire etc) if any
+        if [:before, :notify, :require, :subscribe].include?(type)
+          func = "eachauto#{type}".to_sym
+          if resource.resource_type.respond_to?(func)
+            resource.resource_type.send(func) do |t, b|
+              Array(resource.to_ral.instance_eval(&b)).each do |dep|
+                res = "#{t.to_s.capitalize}[#{dep}]"
+                if r = relationship_refs(res, type, visited)
+                  results << res
+                  results << r
+                end
               end
             end
           end
diff --git a/lib/rspec-puppet/matchers/include_class.rb b/lib/rspec-puppet/matchers/include_class.rb
index f4095fa..f9216b6 100644
--- a/lib/rspec-puppet/matchers/include_class.rb
+++ b/lib/rspec-puppet/matchers/include_class.rb
@@ -4,7 +4,7 @@ module RSpec::Puppet
 
     matcher :include_class do |expected_class|
       match do |catalogue|
-        RSpec.deprecate(:include_class, :replacement => :contain_class)
+        RSpec.deprecate('include_class()', :replacement => 'contain_class()')
         catalogue.call.classes.include?(expected_class)
       end
 
diff --git a/lib/rspec-puppet/matchers/type_matchers.rb b/lib/rspec-puppet/matchers/type_matchers.rb
index 421eb5b..ffde851 100644
--- a/lib/rspec-puppet/matchers/type_matchers.rb
+++ b/lib/rspec-puppet/matchers/type_matchers.rb
@@ -52,9 +52,6 @@ module RSpec::Puppet
         self
       end
 
-      #def with_autorequires(autorequires))
-      #end
-
       #
       # this is the method that drives all of the validation
       #
diff --git a/lib/rspec-puppet/sensitive.rb b/lib/rspec-puppet/sensitive.rb
index 4a660cc..4c933b7 100644
--- a/lib/rspec-puppet/sensitive.rb
+++ b/lib/rspec-puppet/sensitive.rb
@@ -29,7 +29,11 @@ module RSpec::Puppet
       # @param other [#unwrap, Object] value to compare to
       def == other
         if other.respond_to? :unwrap
-          return unwrap == other.unwrap
+          if unwrap.kind_of?(Regexp)
+            return unwrap =~ other.unwrap
+          else
+            return unwrap == other.unwrap
+          end
         else
           super
         end
diff --git a/lib/rspec-puppet/support.rb b/lib/rspec-puppet/support.rb
index f5e7d39..44068f0 100644
--- a/lib/rspec-puppet/support.rb
+++ b/lib/rspec-puppet/support.rb
@@ -392,7 +392,7 @@ module RSpec::Puppet
 
       # Enable app_management by default for Puppet versions that support it
       if Puppet::Util::Package.versioncmp(Puppet.version, '4.3.0') >= 0 && Puppet.version.to_i < 5
-        Puppet[:app_management] = true
+        Puppet[:app_management] = ENV.include?('PUPPET_NOAPP_MANAGMENT') ? false : true
       end
 
       adapter.modulepath.map do |d|
diff --git a/rspec-puppet.gemspec b/rspec-puppet.gemspec
index fe4f200..be5ed83 100644
--- a/rspec-puppet.gemspec
+++ b/rspec-puppet.gemspec
@@ -1,6 +1,6 @@
 Gem::Specification.new do |s|
   s.name = 'rspec-puppet'
-  s.version = '2.8.0'
+  s.version = '2.9.0'
   s.homepage = 'https://github.com/rodjek/rspec-puppet/'
   s.summary = 'RSpec tests for your Puppet manifests'
   s.description = 'RSpec tests for your Puppet manifests'
diff --git a/spec/applications/orch_app_spec.rb b/spec/applications/orch_app_spec.rb
deleted file mode 100644
index 87cec3e..0000000
--- a/spec/applications/orch_app_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-require 'spec_helper'
-
-describe 'orch_app', :if => Puppet::Util::Package.versioncmp(Puppet.version, '4.3.0') >= 0 do
-  let(:node) { 'my_node' }
-  let(:title) { 'my_awesome_app' }
-
-  context 'with params' do
-    let(:params) do
-      {
-        :nodes => {
-          ref('Node', node) => ref('Orch_app::Db', title),
-        },
-        :mystring => 'foobar',
-      }
-    end
-
-    it { should compile }
-    it { should contain_orch_app(title) }
-    it { should contain_orch_app__db(title) }
-  end
-
-  context 'missing params' do
-    it { expect { should compile }.to raise_error(ArgumentError, /provide params for an app/) }
-  end
-end
diff --git a/spec/classes/relationships__type_with_auto.rb b/spec/classes/relationships__type_with_auto.rb
new file mode 100644
index 0000000..9d42da3
--- /dev/null
+++ b/spec/classes/relationships__type_with_auto.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe 'relationships::type_with_auto', :if => Puppet::Util::Package.versioncmp(Puppet.version, '4.0.0') >= 0 do
+  it { is_expected.to compile.with_all_deps }
+  it do
+    is_expected.to contain_type_with_all_auto('/tmp')
+      .that_comes_before('File[/tmp/before]')
+      .that_notifies('File[/tmp/notify]')
+      .that_requires('File[/tmp/require]')
+      .that_subscribes_to('File[/tmp/subscribe]')
+  end
+
+  it { is_expected.to contain_file('/tmp/before').that_requires('Type_with_all_auto[/tmp]') }
+  it { is_expected.to contain_file('/tmp/notify').that_subscribes_to('Type_with_all_auto[/tmp]') }
+  it { is_expected.to contain_file('/tmp/require').that_comes_before('Type_with_all_auto[/tmp]') }
+  it { is_expected.to contain_file('/tmp/subscribe').that_notifies('Type_with_all_auto[/tmp]') }
+end
diff --git a/spec/classes/test_classes_used_spec.rb b/spec/classes/test_classes_used_spec.rb
index de19f2c..caf0db0 100644
--- a/spec/classes/test_classes_used_spec.rb
+++ b/spec/classes/test_classes_used_spec.rb
@@ -2,11 +2,11 @@ require 'spec_helper'
 
 describe 'test::classes_used' do
   it {
-    expect(RSpec).to receive(:deprecate).with(:include_class, {:replacement => :contain_class})
+    expect(RSpec).to receive(:deprecate).with('include_class()', {:replacement => 'contain_class()'})
     should include_class('test::bare_class')
   }
   it {
-    expect(RSpec).to receive(:deprecate).with(:include_class, {:replacement => :contain_class})
+    expect(RSpec).to receive(:deprecate).with('include_class()', {:replacement => 'contain_class()'})
     should include_class('test::parameterised_class')
   }
 
diff --git a/spec/classes/test_sensitive_spec.rb b/spec/classes/test_sensitive_spec.rb
index 5be13ae..ffb8046 100644
--- a/spec/classes/test_sensitive_spec.rb
+++ b/spec/classes/test_sensitive_spec.rb
@@ -2,4 +2,5 @@ require 'spec_helper'
 
 describe 'test::sensitive', :if => Puppet::Util::Package.versioncmp(Puppet.version, '4.6.0') >= 0 do
   it { is_expected.to contain_class('test::sensitive::user').with_password(sensitive('myPassword')) }
+  it { is_expected.to contain_class('test::sensitive::user').with_password(sensitive(%r{Pass})) }
 end
diff --git a/spec/fixtures/modules/relationships/lib/puppet/type/type_with_all_auto.rb b/spec/fixtures/modules/relationships/lib/puppet/type/type_with_all_auto.rb
new file mode 100644
index 0000000..cca2e84
--- /dev/null
+++ b/spec/fixtures/modules/relationships/lib/puppet/type/type_with_all_auto.rb
@@ -0,0 +1,8 @@
+Puppet::Type.newtype(:type_with_all_auto) do
+  ensurable
+  newparam(:name, :namevar => true)
+  autobefore(:file) { [File.join(self[:name], 'before')] }
+  autonotify(:file) { [File.join(self[:name], 'notify')] }
+  autorequire(:file) { [File.join(self[:name], 'require')] }
+  autosubscribe(:file) { [File.join(self[:name], 'subscribe')] }
+end
diff --git a/spec/fixtures/modules/relationships/manifests/type_with_auto.pp b/spec/fixtures/modules/relationships/manifests/type_with_auto.pp
new file mode 100644
index 0000000..b24e5f6
--- /dev/null
+++ b/spec/fixtures/modules/relationships/manifests/type_with_auto.pp
@@ -0,0 +1,9 @@
+# This class is here to test type_with_all_auto which has alll auto* relations
+class relationships::type_with_auto {
+  type_with_all_auto { '/tmp':
+  }
+
+  file { ['/tmp/before', '/tmp/notify', '/tmp/require', '/tmp/subscribe']:
+    ensure => file,
+  }
+}
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 0cf779a..d2491ba 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,10 +1,14 @@
-if ENV['COVERAGE'] == 'yes'
+if ENV['COVERAGE']
   require 'simplecov'
   require 'coveralls'
 
-  SimpleCov.formatter = Coveralls::SimpleCov::Formatter
+  if ENV['COVERAGE'] == 'yes'
+    SimpleCov.formatter = Coveralls::SimpleCov::Formatter
+  end
+
   SimpleCov.start do
-    add_filter(/^\/spec\//)
+    add_filter %r{^/spec/}
+    add_filter %r{^/vendor/}
   end
 end
 
@@ -21,6 +25,13 @@ def sensitive?
   defined?(::Puppet::Pops::Types::PSensitiveType)
 end
 
+module Helpers
+  def rspec2?
+    RSpec::Version::STRING < '3'
+  end
+  module_function :rspec2?
+end
+
 RSpec.configure do |c|
   c.module_path     = File.join(File.dirname(File.expand_path(__FILE__)), 'fixtures', 'modules')
   c.manifest_dir    = File.join(File.dirname(File.expand_path(__FILE__)), 'fixtures', 'manifests')
@@ -31,4 +42,7 @@ RSpec.configure do |c|
   c.after(:suite) do
     RSpec::Puppet::Coverage.report!(0)
   end
+
+  c.include Helpers
+  c.extend Helpers
 end
diff --git a/spec/spec_helper_unit.rb b/spec/spec_helper_unit.rb
new file mode 100644
index 0000000..8eda0c0
--- /dev/null
+++ b/spec/spec_helper_unit.rb
@@ -0,0 +1,49 @@
+if ENV['COVERAGE']
+  require 'coveralls'
+  require 'simplecov'
+
+  if ENV['COVERAGE'] == 'yes'
+    SimpleCov.formatter = Coveralls::SimpleCov::Formatter
+  end
+
+  SimpleCov.start do
+    add_filter %r{^/spec/}
+    add_filter %r{^/vendor/}
+  end
+end
+
+require 'rspec-puppet'
+
+module Helpers
+  def rspec2?
+    RSpec::Version::STRING < '3'
+  end
+  module_function :rspec2?
+
+  def test_double(type, *args)
+    if rspec2?
+      double(type.to_s, *args)
+    else
+      instance_double(type, *args)
+    end
+  end
+end
+
+RSpec.configure do |c|
+  c.include Helpers
+  c.extend Helpers
+
+  if Helpers.rspec2?
+    RSpec::Matchers.define :be_truthy do
+      match do |actual|
+        !!actual == true
+      end
+    end
+
+    RSpec::Matchers.define :be_falsey do
+      match do |actual|
+        !!actual == false
+      end
+    end
+  end
+end
diff --git a/spec/unit/matchers/count_generic_spec.rb b/spec/unit/matchers/count_generic_spec.rb
new file mode 100644
index 0000000..92e869f
--- /dev/null
+++ b/spec/unit/matchers/count_generic_spec.rb
@@ -0,0 +1,220 @@
+require 'spec_helper_unit'
+
+describe RSpec::Puppet::ManifestMatchers::CountGeneric do
+  subject(:matcher) { described_class.new(type, expected, method) }
+
+  let(:actual) do
+    lambda { test_double(Puppet::Resource::Catalog, :resources => resource_objects) }
+  end
+
+  let(:resource_objects) do
+    resources.map do |type, title|
+      test_double(Puppet::Resource, :ref => "#{type}[#{title}]", :type => type)
+    end
+  end
+
+  let(:resources) { [] }
+  let(:type) { nil }
+  let(:expected) { 0 }
+  let(:method) { nil }
+  let(:default_resources) do
+    [
+      ['Class', 'main'],
+      ['Class', 'Settings'],
+      ['Stage', 'main'],
+    ]
+  end
+
+  it 'is not a diffable matcher' do
+    pending 'method not implemented'
+    expect(matcher).not_to be_diffable
+  end
+
+  describe '#initialize' do
+    context 'when initialised with a specified type' do
+      context 'and the type is a single namespace segment' do
+        let(:type) { 'class' }
+
+        it 'capitalises the type' do
+          expect(matcher.resource_type).to eq('Class')
+        end
+      end
+
+      context 'and the type is multiple namespaced segments' do
+        let(:type) { 'test::type' }
+
+        it 'capitalises each segment of the type' do
+          pending 'bug - not implemented'
+          expect(matcher.resource_type).to eq('Test::Type')
+        end
+      end
+    end
+
+    context 'when initialised with a method name via method_missing' do
+      let(:type) { nil }
+
+      context 'and the type is a single namespace segment' do
+        let(:method) { 'have_class_resource_count' }
+
+        it 'extracts the type from the method name and capitalises it' do
+          expect(matcher.resource_type).to eq('Class')
+        end
+      end
+
+      context 'and the type is multiple namespaced segments' do
+        let(:method) { 'have_test__type_resource_count' }
+
+        it 'extracts the type from the method name and capitalises each segment' do
+          expect(matcher.resource_type).to eq('Test::Type')
+        end
+      end
+    end
+  end
+
+  describe '#description' do
+    subject(:description) { matcher.description }
+
+    context 'when counting classes in the catalogue' do
+      let(:type) { 'class' }
+
+      context 'and only a single class is expected' do
+        let(:expected) { 1 }
+
+        it 'describes an expectation of a singular class' do
+          expect(description).to eq('contain exactly 1 class')
+        end
+      end
+
+      context 'and more than one class is expected' do
+        let(:expected) { 2 }
+
+        it 'describes an expectation of plural classes' do
+          expect(description).to eq('contain exactly 2 classes')
+        end
+      end
+    end
+
+    context 'when counting all resources' do
+      let(:type) { 'resource' }
+
+      context 'and only a single resource is expected' do
+        let(:expected) { 1 }
+
+        it 'describes an expectation of a singular resource' do
+          expect(description).to eq('contain exactly 1 resource')
+        end
+      end
+
+      context 'and more than one resource is expected' do
+        let(:expected) { 2 }
+
+        it 'describes an expectation of plural resources' do
+          expect(description).to eq('contain exactly 2 resources')
+        end
+      end
+    end
+
+    context 'when counting resources of a particular type' do
+      let(:type) { 'exec' }
+
+      context 'and only a single resource is expected' do
+        let(:expected) { 1 }
+
+        it 'describes an expectation of a singular resource type' do
+          expect(description).to eq('contain exactly 1 Exec resource')
+        end
+      end
+
+      context 'and more than one resource is expected' do
+        let(:expected) { 2 }
+
+        it 'describes an expectation of plural resources of a type' do
+          expect(description).to eq('contain exactly 2 Exec resources')
+        end
+      end
+    end
+  end
+
+  describe '#matches?' do
+    subject(:match) { matcher.matches?(actual) }
+
+    context 'when counting all resources' do
+      let(:type) { 'resource' }
+      let(:expected) { 0 }
+
+      let(:resources) do
+        [
+          ['Class', 'test'],
+          ['Node', 'testhost.test.com'],
+        ]
+      end
+
+      it 'does not include Class, Node or default resources in the count' do
+        expect(match).to be_truthy
+      end
+
+      context 'and the catalogue contains a number of countable resources' do
+        let(:resources) do
+          super() + [
+            ['File', '/tmp/testfile'],
+            ['Exec', 'some command'],
+            ['Service', 'a service'],
+          ]
+        end
+
+        context 'and the expected value matches the resource count' do
+          let(:expected) { 3 }
+
+          it 'returns true' do
+            expect(match).to be_truthy
+          end
+        end
+
+        context 'and the expected value does not match the resource count' do
+          let(:expected) { 4 }
+
+          it 'returns false' do
+            expect(match).to be_falsey
+          end
+        end
+      end
+    end
+
+    context 'and counting resources of a particular type' do
+      let(:type) { 'class' }
+      let(:expected) { 1 }
+      let(:resources) do
+        [
+          ['Class', 'test'],
+          ['File', 'testfile'],
+        ]
+      end
+
+      it 'does not include default resources of that type in the resource count' do
+        expect(match).to be_truthy
+      end
+    end
+  end
+
+  describe '#failure_message' do
+    let(:expected) { 999 }
+    let(:type) { 'class' }
+
+    it 'provides the description of the failure and the actual value' do
+      matcher.matches?(actual)
+      msg = 'expected that the catalogue would contain exactly 999 classes but it contains 0'
+      expect(matcher.failure_message).to eq(msg)
+    end
+  end
+
+  describe '#failure_message_when_negated' do
+    let(:expected) { 999 }
+    let(:type) { 'class' }
+
+    it 'provides the description of the failure' do
+      matcher.matches?(actual)
+      msg = 'expected that the catalogue would not contain exactly 999 classes but it does'
+      expect(matcher.failure_message_when_negated).to eq(msg)
+    end
+  end
+end
diff --git a/spec/unit/matchers/have_class_count_spec.rb b/spec/unit/matchers/have_class_count_spec.rb
new file mode 100644
index 0000000..c37d266
--- /dev/null
+++ b/spec/unit/matchers/have_class_count_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe 'RSpec::Puppet::ManifestMatchers.have_class_count' do
+  subject(:example_group) { Class.new { extend RSpec::Puppet::ManifestMatchers } }
+  let(:expected) { 123 }
+
+  after do
+    example_group.have_class_count(expected)
+  end
+
+  it 'initialises a CountGeneric matcher for Class resources' do
+    expect(RSpec::Puppet::ManifestMatchers::CountGeneric).to receive(:new).with('class', expected)
+  end
+end
diff --git a/spec/unit/matchers/have_resource_count_spec.rb b/spec/unit/matchers/have_resource_count_spec.rb
new file mode 100644
index 0000000..13b3b56
--- /dev/null
+++ b/spec/unit/matchers/have_resource_count_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe 'RSpec::Puppet::ManifestMatchers.have_resource_count' do
+  subject(:example_group) { Class.new { extend RSpec::Puppet::ManifestMatchers } }
+  let(:expected) { 123 }
+
+  after do
+    example_group.have_resource_count(expected)
+  end
+
+  it 'initialises a CountGeneric matcher for all resources' do
+    expect(RSpec::Puppet::ManifestMatchers::CountGeneric).to receive(:new).with('resource', expected)
+  end
+end
diff --git a/spec/unit/matchers/include_class_spec.rb b/spec/unit/matchers/include_class_spec.rb
new file mode 100644
index 0000000..4af1e7a
--- /dev/null
+++ b/spec/unit/matchers/include_class_spec.rb
@@ -0,0 +1,78 @@
+require 'spec_helper_unit'
+
+describe 'RSpec::Puppet::ManifestMatchers.include_class' do
+  subject(:matcher) { Class.new { extend RSpec::Puppet::ManifestMatchers }.include_class(expected) }
+
+  let(:actual) do
+    lambda { test_double(Puppet::Resource::Catalog, :classes => included_classes) }
+  end
+
+  let(:expected) { 'test_class' }
+  let(:included_classes) { [] }
+
+  it 'is not a diffable matcher' do
+    expect(matcher).not_to be_diffable
+  end
+
+  before do
+    allow(RSpec).to receive(:deprecate).with('include_class()', :replacement => 'contain_class()')
+  end
+
+  describe '#description' do
+    it 'includes the expected class name' do
+      expect(matcher.description).to eq("include Class[#{expected}]")
+    end
+  end
+
+  describe '#matches?' do
+    context 'when the catalogue includes the expected class' do
+      let(:included_classes) { [expected] }
+
+      it 'returns true' do
+        expect(matcher.matches?(actual)).to be_truthy
+      end
+    end
+
+    context 'when the catalogue does not include the expected class' do
+      let(:included_classes) { ['something_else'] }
+
+      it 'returns false' do
+        expect(matcher.matches?(actual)).to be_falsey
+      end
+    end
+  end
+
+  describe '#failure_message_for_should', :if => rspec2? do
+    it 'provides a description and the expected class' do
+      matcher.matches?(actual)
+      expect(matcher.failure_message_for_should).to eq("expected that the catalogue would include Class[#{expected}]")
+    end
+  end
+
+  describe '#failure_message', :unless => rspec2? do
+    it 'provides a description and the expected class' do
+      matcher.matches?(actual)
+      expect(matcher.failure_message).to eq("expected that the catalogue would include Class[#{expected}]")
+    end
+  end
+
+  describe '#failure_message_for_should_not', :if => rspec2? do
+    let(:included_classes) { [expected] }
+
+    it 'provides a description and the expected class' do
+      pending 'not implemented'
+      matcher.matches?(actual)
+      expect(matcher.failure_message_when_negated).to eq("expected that the catalogue would not include Class[#{expected}]")
+    end
+  end
+
+  describe '#failure_message_when_negated', :unless => rspec2? do
+    let(:included_classes) { [expected] }
+
+    it 'provides a description and the expected class' do
+      pending 'not implemented'
+      matcher.matches?(actual)
+      expect(matcher.failure_message_when_negated).to eq("expected that the catalogue would not include Class[#{expected}]")
+    end
+  end
+end
diff --git a/spec/unit/matchers/run_spec.rb b/spec/unit/matchers/run_spec.rb
index f7abae4..188c517 100644
--- a/spec/unit/matchers/run_spec.rb
+++ b/spec/unit/matchers/run_spec.rb
@@ -1,97 +1,45 @@
-require 'spec_helper'
+require 'spec_helper_unit'
 
 describe RSpec::Puppet::FunctionMatchers::Run do
-  let (:wrapper) { double("function wrapper") }
+  subject(:matcher) { described_class.new }
 
-  before :each do
-    expect(wrapper).to receive(:call).never
-  end
-
-  it 'should call the wrapper with no params' do
-    expect(wrapper).to receive(:execute).with(no_args).once
-    expect(subject.matches?(wrapper)).to be true
-  end
-
-  it 'should not match a wrapper that raises an error' do
-    expect(wrapper).to receive(:execute).and_raise(StandardError, 'Forced Error').once
-    expect(subject.matches?(wrapper)).to be false
-  end
-
-  [ [true], [false], [''], ['string'], [nil], [0], [1.1], [[]], ['one', 'two'], [{}], [{ 'key' => 'value' }], [:undef] ].each do |supplied_params|
-    context "with_params(#{supplied_params.collect { |p| p.inspect }.join(', ')})" do
-      before(:each) { subject.with_params(*supplied_params) }
-
-      it 'should call the wrapper with the supplied params' do
-        expect(wrapper).to receive(:execute).with(*supplied_params).once
-        expect(subject.matches?(wrapper)).to be true
-      end
-
-      it 'should not match a wrapper that raises an error' do
-        expect(wrapper).to receive(:execute).and_raise(StandardError, 'Forced Error').once
-        expect(subject.matches?(wrapper)).to be false
-      end
-    end
+  let(:wrapper) { test_double(RSpec::Puppet::FunctionExampleGroup::V4FunctionWrapper) }
 
-    [ true, false, '', 'string', nil, 0, 1.1, [], {}, :undef ].each do |expected_return|
-      context "and_return(#{expected_return.inspect})" do
-        before(:each) { subject.and_return(expected_return) }
-
-        it 'should match a wrapper that does return the requested value' do
-          expect(wrapper).to receive(:execute).and_return(expected_return).once
-          expect(subject.matches?(wrapper)).to be true
+  describe '#matches?' do
+    context 'when the function takes no arguments and has no expected return value' do
+      context 'and returns nothing' do
+        it 'returns true' do
+          allow(wrapper).to receive(:execute).with(no_args).once
+          expect(matcher.matches?(wrapper)).to be_truthy
         end
+      end
 
-        it 'should not match a wrapper that does return a different value' do
-          expect(wrapper).to receive(:execute).and_return(!expected_return).once
-          expect(subject.matches?(wrapper)).to be false
+      context 'and raises an exception' do
+        it 'returns false' do
+          allow(wrapper).to receive(:execute).with(no_args).and_raise(StandardError, 'Forced Error').once
+          expect(matcher.matches?(wrapper)).to be_falsey
         end
       end
+    end
+  end
 
-      context "and_raise_error(ArgumentError)" do
-        before(:each) { subject.and_raise_error(ArgumentError) }
+  describe '#with_params' do
 
-        it 'should match a wrapper that raises ArgumentError' do
-          expect(wrapper).to receive(:execute).and_raise(ArgumentError, 'Forced Error').once
-          expect(subject.matches?(wrapper)).to be true
-        end
+  end
 
-        [ true, false, '', 'string', nil, 0, 1.1, [], {}, :undef ].each do |value|
-          it "should not match a wrapper that returns #{value.inspect}" do
-            expect(wrapper).to receive(:execute).and_return(value).once
-            expect(subject.matches?(wrapper)).to be false
-          end
-        end
+  describe '#with_lambda' do
+    context 'when a lambda is passed to the matcher' do
+      let(:block) { proc { |r| r + 2 } }
 
-        it 'should not match a wrapper that raises a different error' do
-          expect(wrapper).to receive(:execute).and_raise(StandardError, 'Forced Error').once
-          expect(subject.matches?(wrapper)).to be false
-        end
+      before do
       end
 
-      context "and_raise_error(ArgumentError, /message/)" do
-        before(:each) { subject.and_raise_error(ArgumentError, /message/) }
-
-        it 'should match a wrapper that raises ArgumentError("with matching message")' do
-          expect(wrapper).to receive(:execute).and_raise(ArgumentError, 'with matching message').once
-          expect(subject.matches?(wrapper)).to be true
-        end
-
-        it 'should not match a wrapper that raises a different ArgumentError' do
-          expect(wrapper).to receive(:execute).and_raise(ArgumentError, 'Forced Error').once
-          expect(subject.matches?(wrapper)).to be false
-        end
-
-        [ true, false, '', 'string', nil, 0, 1.1, [], {}, :undef ].each do |value|
-          it "should not match a wrapper that returns #{value.inspect}" do
-            expect(wrapper).to receive(:execute).and_return(value).once
-            expect(subject.matches?(wrapper)).to be false
-          end
-        end
+      after do
+      end
 
-        it 'should not match a wrapper that raises a different error' do
-          expect(wrapper).to receive(:execute).and_raise(StandardError, 'Forced Error').once
-          expect(subject.matches?(wrapper)).to be false
-        end
+      it 'passes the lambda when executing the function' do
+        matcher.with_lambda { |r| r + 2 }
+        matcher.matches?(wrapper)
       end
     end
   end
diff --git a/spec/unit/raw_string_spec.rb b/spec/unit/raw_string_spec.rb
new file mode 100644
index 0000000..f7fd739
--- /dev/null
+++ b/spec/unit/raw_string_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper_unit'
+
+describe RSpec::Puppet::RawString do
+  subject(:raw_string) { described_class.new('some string') }
+
+  describe '#inspect' do
+    it 'returns an unquoted version of the string' do
+      expect(raw_string.inspect).to eq('some string')
+    end
+  end
+end