diff --git a/Gemfile b/Gemfile
index 45e3cc721b3345ad5df53b38a8ee5b47220cdb68..4be409b50b8c8530a9373f6ec372e00a589287c8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -8,3 +8,4 @@ gemspec
 gem "minitest", "~> 5.0"
 gem "rake", "~> 13.0"
 gem "rubocop", "~> 1.21"
+gem "webmock"
diff --git a/lib/liudesk_cmdb/models/generic.rb b/lib/liudesk_cmdb/models/generic.rb
new file mode 100644
index 0000000000000000000000000000000000000000..90ce93e4b3e9b933613d1150c4f12a014f16d353
--- /dev/null
+++ b/lib/liudesk_cmdb/models/generic.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+# Storage for generic model components
+module LiudeskCMDB::Models::Generic
+  autoload :Client, "liudesk_cmdb/models/generic/client"
+end
diff --git a/lib/liudesk_cmdb/models/generic/client.rb b/lib/liudesk_cmdb/models/generic/client.rb
new file mode 100644
index 0000000000000000000000000000000000000000..38ab3871822b31cecdd81bc297cd3743b8605ec8
--- /dev/null
+++ b/lib/liudesk_cmdb/models/generic/client.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module LiudeskCMDB::Models::Generic
+  # Generic client model, for Linux/Windows/Mac Client/ComputerLab assets
+  #
+  # @!attribute [r] created_date
+  #   @return [Time] the date of creation
+  # @!attribute [r] updated_date
+  #   @return [Time] the date of change
+  # @!attribute hardware_id
+  #   @return [String] the ID for the underlying HardwareV1 object
+  # @!attribute hostname
+  #   @return [String] the hostname of the client
+  # @!attribute division
+  #   @return [String?] the division the client belongs to
+  module Client
+    def self.included(base)
+      base.class_eval do
+        identifier :hostname
+
+        access_fields \
+          :hostname, :division, :asset_owner, :certificate_information,
+          :network_access_role, :hardware_id,
+          :operating_system_type, :operating_system, :operating_system_install_date,
+          :ad_creation_date, :active_directory_ou, :client_classification,
+          :management_system, :management_system_id, :network_certificate_ca,
+          :misc_information
+
+        field_attributes :hostname, name: "hostName"
+        field_attributes :hardware_id, name: "hardwareID"
+        field_attributes :active_directory_ou, name: "activeDirectoryOU"
+        field_attributes :operating_system_install_date, convert: Time
+        field_attributes :ad_creation_date, convert: Time
+      end
+    end
+
+    def to_s
+      hostname
+    end
+
+    # Get an instance of the underlying hardware object
+    #
+    # @return [HardwareV1] the hardware underlying this client
+    def hardware
+      @hardware ||= LiudeskCMDB::Models::HardwareV1.get(client, hardware_id)
+    end
+
+    # Change the underlying hardware object
+    #
+    # @param hardware [HardwareV1] the new hardware object to assign
+    def hardware=(hardware)
+      raise ArgumentError, "Must be a HardwareV1" unless hardware.is_a? LinudeskCMDB::Models::HardwareV1
+
+      self.hardware_id = hardware.guid if hardware
+      @hardware = hardware
+    end
+  end
+end
diff --git a/lib/liudesk_cmdb/models/linux_client.rb b/lib/liudesk_cmdb/models/linux_client.rb
index 8ced5f52625cc47365c21882e0170844072aa757..de6aef4919d6b8c2da06c191f45263dd15036f14 100644
--- a/lib/liudesk_cmdb/models/linux_client.rb
+++ b/lib/liudesk_cmdb/models/linux_client.rb
@@ -2,56 +2,11 @@
 
 module LiudeskCMDB::Models
   # Linux Client v1
-  #
-  # @!attribute [r] created_date
-  #   @return [Time] the date of creation
-  # @!attribute [r] updated_date
-  #   @return [Time] the date of change
-  # @!attribute hardware_id
-  #   @return [String] the ID for the underlying HardwareV1 object
-  # @!attribute hostname
-  #   @return [String] the hostname of the client
-  # @!attribute division
-  #   @return [String?] the division the client belongs to
   class LinuxClientV1 < LiudeskCMDB::Model
+    include LiudeskCMDB::Models::Generic::Client
+
     api_name "Clients"
     model_name "linux"
     model_version :v1
-
-    identifier :hostname
-
-    access_fields \
-      :hostname, :division, :asset_owner, :certificate_information,
-      :network_access_role, :hardware_id,
-      :operating_system_type, :operating_system, :operating_system_install_date,
-      :ad_creation_date, :active_directory_ou, :client_classification,
-      :management_system, :management_system_id, :network_certificate_ca
-
-    field_attributes :hostname, name: "hostName"
-    field_attributes :hardware_id, name: "hardwareID"
-    field_attributes :active_directory_ou, name: "activeDirectoryOU"
-    field_attributes :operating_system_install_date, convert: Time
-    field_attributes :ad_creation_date, convert: Time
-
-    def to_s
-      hostname
-    end
-
-    # Get an instance of the underlying hardware object
-    #
-    # @return [HardwareV1] the hardware underlying this client
-    def hardware
-      @hardware ||= LiudeskCMDB::Models::HardwareV1.get(client, hardware_id)
-    end
-
-    # Change the underlying hardware object
-    #
-    # @param hardware [HardwareV1] the new hardware object to assign
-    def hardware=(hardware)
-      raise ArgumentError, "Must be a HardwareV1" unless hardware.is_a? LinudeskCMDB::Models::HardwareV1
-
-      self.hardware_id = hardware.guid if hardware
-      @hardware = hardware
-    end
   end
 end
diff --git a/lib/liudesk_cmdb/models/linux_computerlab.rb b/lib/liudesk_cmdb/models/linux_computerlab.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3c57c18f3f9061ba1d5fb5f04ba8682560f5270c
--- /dev/null
+++ b/lib/liudesk_cmdb/models/linux_computerlab.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module LiudeskCMDB::Models
+  # Linux Client v1
+  class LinuxComputerlabV1 < LiudeskCMDB::Model
+    include LiudeskCMDB::Models::Generic::Client
+
+    api_name "ComputerLabs"
+    model_name "linux"
+    model_version :v1
+  end
+end
diff --git a/lib/liudesk_cmdb/models/mac_client.rb b/lib/liudesk_cmdb/models/mac_client.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ae474cf3e6ba0454cc9143e88cf27dfa78bc29f3
--- /dev/null
+++ b/lib/liudesk_cmdb/models/mac_client.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module LiudeskCMDB::Models
+  # Mac Client v1
+  class MacClientV1 < LiudeskCMDB::Model
+    include LiudeskCMDB::Models::Generic::Client
+
+    api_name "Clients"
+    model_name "mac"
+    model_version :v1
+  end
+end
diff --git a/lib/liudesk_cmdb/models/windows_client.rb b/lib/liudesk_cmdb/models/windows_client.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3a8735e87cc1448dcebe1c900830df93879110cd
--- /dev/null
+++ b/lib/liudesk_cmdb/models/windows_client.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module LiudeskCMDB::Models
+  # Windows Client v1
+  class WindowsClientV1 < LiudeskCMDB::Model
+    include LiudeskCMDB::Models::Generic::Client
+
+    api_name "Clients"
+    model_name "windows"
+    model_version :v1
+  end
+end
diff --git a/lib/liudesk_cmdb/models/windows_computerlab.rb b/lib/liudesk_cmdb/models/windows_computerlab.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6d5829a51a0c63182218b43887cc3feb00cd35e7
--- /dev/null
+++ b/lib/liudesk_cmdb/models/windows_computerlab.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module LiudeskCMDB::Models
+  # Windows ComputerLab v1
+  class WindowsComputerlabV1 < LiudeskCMDB::Model
+    include LiudeskCMDB::Models::Generic::Client
+
+    api_name "ComputerLabs"
+    model_name "windows"
+    model_version :v1
+  end
+end
diff --git a/test/fixtures/get_linuxclient.json b/test/fixtures/get_linuxclient.json
new file mode 100644
index 0000000000000000000000000000000000000000..de456f7e687aba3f78d15e2e83b34e6c1e225c5c
--- /dev/null
+++ b/test/fixtures/get_linuxclient.json
@@ -0,0 +1,20 @@
+{
+    "managementSystem": "",
+    "managementSystemId": "",
+    "createdDate": "2023-05-22T13:47:50.240Z",
+    "updatedDate": "2023-07-03T07:14:33.794Z",
+    "archived": false,
+    "clientClassification": null,
+    "networkCertificateCa": null,
+    "adCreationDate": null,
+    "activeDirectoryOU": null,
+    "hardwareID": "2f90ced1-1916-45db-b2cf-a6ad66f69aba",
+    "operatingSystemType": "lnx Red Hat Enterprise Linux release 8.8 (Ootpa)",
+    "operatingSystem": "8.8",
+    "operatingSystemInstallDate": null,
+    "hostName": "client.localhost.localdomain",
+    "certificateInformation": null,
+    "networkAccessRole": "None",
+    "division": null,
+    "assetOwner": null
+}
diff --git a/test/test_helper.rb b/test/test_helper.rb
index efaf3ddadfc948326ff8f31ab173705d141342ed..1909694d7674f1aaca534fe59f4599b7f614688a 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -6,3 +6,8 @@ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
 require "liudesk_cmdb"
 
 require "minitest/autorun"
+require "webmock/minitest"
+
+def setup_cmdb_client
+  LiudeskCMDB::Client.new 'https://example.com', subscription_key: 'testing'
+end
diff --git a/test/test_v1_models.rb b/test/test_v1_models.rb
index ac56576eea7336328213a5123efd037f2d1b540e..a0cdf4027abde9c4ecdea64a55b3ce06758a79d2 100644
--- a/test/test_v1_models.rb
+++ b/test/test_v1_models.rb
@@ -4,7 +4,7 @@ require "test_helper"
 
 class TestV1Models < Minitest::Test
   def setup
-    @client = Minitest::Mock.new
+    @client = setup_cmdb_client
   end
 
   def test_that_hardware_works
@@ -19,22 +19,21 @@ class TestV1Models < Minitest::Test
     assert_raises(NoMethodError) { hardware.created_date = "Something else" }
     assert_raises(NoMethodError) { hardware.updated_date = "Something else" }
 
-    @client.expect(
-      :post, File.read("test/fixtures/post_hardware.json"),
-      [
-        "liudesk-cmdb/api/Hardware", :v1,
-        {
-          "make" => "HP",
-          "model" => "Elitebook",
-          "serialNumber" => "abc123",
-          "purchaseDate" => "2023-06-12T11:58:05.000Z"
-        }
-      ]
+    stub_post = stub_request(:post, "https://example.com/liudesk-cmdb/api/Hardware").with(
+      body: {
+        make: "HP",
+        model: "Elitebook",
+        serialNumber: "abc123",
+        purchaseDate: "2023-06-12T11:58:05.000Z"
+      }
+    ).to_return(
+      status: 200,
+      body: File.read("test/fixtures/post_hardware.json")
     )
 
     hardware.create
 
-    @client.verify
+    assert_requested stub_post
 
     refute hardware.unknown_fields?
     assert_equal "HP", hardware.make
@@ -66,19 +65,18 @@ class TestV1Models < Minitest::Test
 
     hardware.supplier = "Dustin"
 
-    @client.expect(
-      :patch, File.read("test/fixtures/post_hardware.json"),
-      [
-        "liudesk-cmdb/api/Hardware/8e2cbcf0-1c79-4b4e-8813-71f8da3c3d81", :v1,
-        {
-          "supplier" => "Dustin"
-        }
-      ]
+    stub_patch = stub_request(:patch, "https://example.com/liudesk-cmdb/api/Hardware/8e2cbcf0-1c79-4b4e-8813-71f8da3c3d81").with(
+      body: {
+        supplier: "Dustin"
+      }
+    ).to_return(
+      status: 200,
+      body: File.read("test/fixtures/post_hardware.json")
     )
 
     hardware.update
 
-    @client.verify
+    assert_requested stub_patch
 
     assert_equal "Guest", hardware.network_access_role
     assert_equal "ATEA", hardware.supplier
@@ -87,39 +85,35 @@ class TestV1Models < Minitest::Test
 
     assert_equal "Dustin", hardware.supplier
 
-    @client.expect(
-      :get, File.read("test/fixtures/post_hardware.json"),
-      [
-        "liudesk-cmdb/api/Hardware/8e2cbcf0-1c79-4b4e-8813-71f8da3c3d81", :v1
-      ]
+    stub_get = stub_request(:get, "https://example.com/liudesk-cmdb/api/Hardware/8e2cbcf0-1c79-4b4e-8813-71f8da3c3d81").to_return(
+      status: 200,
+      body: File.read("test/fixtures/post_hardware.json")
     )
 
     hardware.refresh
 
-    @client.verify
+    assert_requested stub_get
 
     assert_equal "ATEA", hardware.supplier
 
-    @client.expect(
-      :delete, nil,
-      [
-        "liudesk-cmdb/api/Hardware/8e2cbcf0-1c79-4b4e-8813-71f8da3c3d81", :v1
-      ]
+    stub_delete = stub_request(:delete, "https://example.com/liudesk-cmdb/api/Hardware/8e2cbcf0-1c79-4b4e-8813-71f8da3c3d81").to_return(
+      status: 204,
     )
 
     hardware.destroy
 
-    @client.verify
+    assert_requested stub_delete
 
-    @client.expect(:get, File.read("test/fixtures/search_hardware.json")) do |path, ver, **args|
-      path == "liudesk-cmdb/api/Hardware/search" && ver == :v1 && args == { query: {
-        query: "make==HP"
-      } }
-    end
+    stub_get = stub_request(:get, "https://example.com/liudesk-cmdb/api/Hardware/search").with(
+      query: { query: "make==HP" }
+    ).to_return(
+      status: 200,
+      body: File.read("test/fixtures/search_hardware.json")
+    )
 
     hardwares = LiudeskCMDB::Models::HardwareV1.search(@client, make: "HP")
 
-    @client.verify
+    assert_requested stub_get
 
     hardware = hardwares.first
 
@@ -153,13 +147,14 @@ class TestV1Models < Minitest::Test
   end
 
   def test_that_server_works
-    @client.expect(:get, File.read("test/fixtures/list_servers.json")) do |path, ver, **args|
-      path == "liudesk-cmdb/api/Server" && ver == :v1 && args == { query: {} }
-    end
+    stub_get = stub_request(:get, "https://example.com/liudesk-cmdb/api/Server").to_return(
+      status: 200,
+      body: File.read("test/fixtures/list_servers.json")
+    )
 
     servers = LiudeskCMDB::Models::ServerV1.list(@client)
 
-    @client.verify
+    assert_requested stub_get
 
     server = servers.first
 
@@ -185,16 +180,14 @@ class TestV1Models < Minitest::Test
     assert_equal Time.parse("2023-04-06T14:15:58Z"), server.created_date
     assert_equal Time.parse("2023-04-06T14:15:58Z"), server.updated_date
 
-    @client.expect(
-      :get, File.read("test/fixtures/post_hardware.json"),
-      [
-        "liudesk-cmdb/api/Hardware/8e2cbcf0-1c79-4b4e-8813-71f8da3c3d81", :v1
-      ]
+    stub_get = stub_request(:get, "https://example.com/liudesk-cmdb/api/Hardware/8e2cbcf0-1c79-4b4e-8813-71f8da3c3d81").to_return(
+      status: 200,
+      body: File.read("test/fixtures/post_hardware.json")
     )
 
     hardware = server.hardware
 
-    @client.verify
+    assert_requested stub_get
 
     refute hardware.unknown_fields?
     assert_equal "HP", hardware.make
@@ -228,17 +221,68 @@ class TestV1Models < Minitest::Test
   def test_handling_of_unexpected_data
     server = LiudeskCMDB::Models::ServerV1.new(@client, hostname: 'test.example.com')
 
-    @client.expect(:get, '{"hostName":"test.example.com","networkAccessRole":"None","unknownField":"Data"}') do |path, ver, **args|
-      path == "liudesk-cmdb/api/Server/test.example.com" && ver == :v1
-    end
+    stub_get = stub_request(:get, "https://example.com/liudesk-cmdb/api/Server/test.example.com").to_return(
+      status: 200,
+      body: {
+        hostName: "test.example.com",
+        networkAccessRole: "None",
+        unknownField: "Data"
+      }.to_json
+    )
 
     assert server.refresh
 
-    @client.verify
+    assert_requested stub_get
 
     assert server.unknown_fields?
     assert_equal "test.example.com", server.hostname
     assert_equal "None", server.network_access_role
     assert_equal "Data", server.unknown_fields["unknownField"]
   end
+
+  def test_handling_of_clients
+    models = {
+      LiudeskCMDB::Models::LinuxClientV1 => [
+        {
+          method: :get,
+          request: :get,
+          url: 'liudesk-cmdb/api/Clients/linux/%<identifier>s',
+          body: 'test/fixtures/get_linuxclient.json'
+        }
+      ],
+      LiudeskCMDB::Models::MacClientV1 => [
+      ],
+      LiudeskCMDB::Models::WindowsClientV1 => [
+      ],
+      LiudeskCMDB::Models::LinuxComputerlabV1 => [
+      ],
+      # LiudeskCMDB::Models::MacComputerlabV1,
+      LiudeskCMDB::Models::WindowsComputerlabV1 => [
+      ],
+    }
+
+    identifier = "client.localhost.localdomain"
+    models.each do |klass, requests|
+      instance = klass.new(@client, identifier)
+
+      assert instance
+
+      requests.each do |req|
+        stub = stub_request(req[:request], "https://example.com/#{req[:url] % { identifier: identifier }}").to_return(
+          status: req.fetch(:status, 200),
+          body: req.fetch(:body_data, File.read(req[:body]))
+        )
+
+        result = nil
+        case req[:method]
+        when :get
+          result = klass.get(@client, identifier)
+        end
+
+        assert_requested stub
+
+        assert result unless req[:method] == :delete
+      end
+    end
+  end
 end