Separate out AttributeValueEncoder/Decoder test boilerplate helper logic into separate library (#35678)

* Split out read/write support for data model provider testing

* Remove one more unused file that got moved

* Undo submodule update

---------

Co-authored-by: Andrei Litvin <andreilitvin@google.com>
diff --git a/src/app/codegen-data-model-provider/tests/AttributeReportIBEncodeDecode.h b/src/app/codegen-data-model-provider/tests/AttributeReportIBEncodeDecode.h
deleted file mode 100644
index 83c079f..0000000
--- a/src/app/codegen-data-model-provider/tests/AttributeReportIBEncodeDecode.h
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- *    Copyright (c) 2024 Project CHIP Authors
- *    All rights reserved.
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- */
-#pragma once
-
-#include <app/ConcreteAttributePath.h>
-#include <app/MessageDef/AttributeDataIB.h>
-#include <app/MessageDef/AttributeReportIBs.h>
-#include <app/MessageDef/ReportDataMessage.h>
-#include <lib/core/DataModelTypes.h>
-#include <lib/core/TLVReader.h>
-#include <lib/core/TLVWriter.h>
-
-#include <vector>
-
-namespace chip {
-namespace Test {
-
-struct DecodedAttributeData
-{
-    chip::DataVersion dataVersion;
-    chip::app::ConcreteDataAttributePath attributePath;
-    chip::TLV::TLVReader dataReader;
-
-    CHIP_ERROR DecodeFrom(const chip::app::AttributeDataIB::Parser & parser);
-};
-
-CHIP_ERROR DecodeAttributeReportIBs(ByteSpan data, std::vector<DecodedAttributeData> & decoded_items);
-
-/// Maintains an internal TLV buffer for data encoding and
-/// decoding for ReportIBs.
-///
-/// Main use case is that explicit TLV layouts (structure and container starting) need to be
-/// prepared to have a proper AttributeReportIBs::Builder/parser to exist.
-class EncodedReportIBs
-{
-public:
-    /// Initialize the report structures required to encode a
-    CHIP_ERROR StartEncoding(app::AttributeReportIBs::Builder & builder);
-    CHIP_ERROR FinishEncoding(app::AttributeReportIBs::Builder & builder);
-
-    /// Decode the embedded attribute report IBs.
-    /// The TLVReaders inside data have a lifetime tied to the current object (its readers point
-    /// inside the current object)
-    CHIP_ERROR Decode(std::vector<DecodedAttributeData> & decoded_items);
-
-private:
-    uint8_t mTlvDataBuffer[1024];
-    TLV::TLVType mOuterStructureType;
-    TLV::TLVWriter mEncodeWriter;
-    ByteSpan mDecodeSpan;
-};
-
-} // namespace Test
-} // namespace chip
diff --git a/src/app/codegen-data-model-provider/tests/BUILD.gn b/src/app/codegen-data-model-provider/tests/BUILD.gn
index 247685a..d8f8216 100644
--- a/src/app/codegen-data-model-provider/tests/BUILD.gn
+++ b/src/app/codegen-data-model-provider/tests/BUILD.gn
@@ -22,8 +22,6 @@
     # data-model access and ember-compatibility (we share the same buffer)
     "${chip_root}/src/app/util/ember-global-attribute-access-interface.cpp",
     "${chip_root}/src/app/util/ember-io-storage.cpp",
-    "AttributeReportIBEncodeDecode.cpp",
-    "AttributeReportIBEncodeDecode.h",
     "EmberInvokeOverride.cpp",
     "EmberInvokeOverride.h",
     "EmberReadWriteOverride.cpp",
@@ -32,6 +30,7 @@
   ]
 
   public_deps = [
+    "${chip_root}/src/app/data-model-provider/tests:encode-decode",
     "${chip_root}/src/app/util/mock:mock_ember",
     "${chip_root}/src/protocols",
   ]
diff --git a/src/app/codegen-data-model-provider/tests/TestCodegenModelViaMocks.cpp b/src/app/codegen-data-model-provider/tests/TestCodegenModelViaMocks.cpp
index 164d885..077376b 100644
--- a/src/app/codegen-data-model-provider/tests/TestCodegenModelViaMocks.cpp
+++ b/src/app/codegen-data-model-provider/tests/TestCodegenModelViaMocks.cpp
@@ -19,7 +19,6 @@
 
 #include <pw_unit_test/framework.h>
 
-#include <app/codegen-data-model-provider/tests/AttributeReportIBEncodeDecode.h>
 #include <app/codegen-data-model-provider/tests/EmberInvokeOverride.h>
 #include <app/codegen-data-model-provider/tests/EmberReadWriteOverride.h>
 
@@ -38,6 +37,9 @@
 #include <app/codegen-data-model-provider/CodegenDataModelProvider.h>
 #include <app/data-model-provider/OperationTypes.h>
 #include <app/data-model-provider/StringBuilderAdapters.h>
+#include <app/data-model-provider/tests/ReadTesting.h>
+#include <app/data-model-provider/tests/TestConstants.h>
+#include <app/data-model-provider/tests/WriteTesting.h>
 #include <app/data-model/Decode.h>
 #include <app/data-model/Encode.h>
 #include <app/data-model/Nullable.h>
@@ -63,6 +65,7 @@
 using namespace chip;
 using namespace chip::Test;
 using namespace chip::app;
+using namespace chip::app::Testing;
 using namespace chip::app::DataModel;
 using namespace chip::app::Clusters::Globals::Attributes;
 
@@ -70,9 +73,6 @@
 
 namespace {
 
-constexpr FabricIndex kTestFabrixIndex = kMinValidFabricIndex;
-constexpr NodeId kTestNodeId           = 0xFFFF'1234'ABCD'4321;
-
 constexpr AttributeId kAttributeIdReadOnly   = 0x3001;
 constexpr AttributeId kAttributeIdTimedWrite = 0x3002;
 
@@ -88,23 +88,6 @@
 static_assert(kEndpointIdThatIsMissing != kMockEndpoint2);
 static_assert(kEndpointIdThatIsMissing != kMockEndpoint3);
 
-constexpr Access::SubjectDescriptor kAdminSubjectDescriptor{
-    .fabricIndex = kTestFabrixIndex,
-    .authMode    = Access::AuthMode::kCase,
-    .subject     = kTestNodeId,
-};
-constexpr Access::SubjectDescriptor kViewSubjectDescriptor{
-    .fabricIndex = kTestFabrixIndex + 1,
-    .authMode    = Access::AuthMode::kCase,
-    .subject     = kTestNodeId,
-};
-
-constexpr Access::SubjectDescriptor kDenySubjectDescriptor{
-    .fabricIndex = kTestFabrixIndex + 2,
-    .authMode    = Access::AuthMode::kCase,
-    .subject     = kTestNodeId,
-};
-
 bool operator==(const Access::SubjectDescriptor & a, const Access::SubjectDescriptor & b)
 {
     if (a.fabricIndex != b.fabricIndex)
@@ -605,108 +588,6 @@
     T mData;
 };
 
-/// Contains a `ReadAttributeRequest` as well as classes to convert this into a AttributeReportIBs
-/// and later decode it
-///
-/// It wraps boilerplate code to obtain a `AttributeValueEncoder` as well as later decoding
-/// the underlying encoded data for verification.
-struct TestReadRequest
-{
-    ReadAttributeRequest request;
-
-    // encoded-used classes
-    EncodedReportIBs encodedIBs;
-    AttributeReportIBs::Builder reportBuilder;
-    std::unique_ptr<AttributeValueEncoder> encoder;
-
-    TestReadRequest(const Access::SubjectDescriptor & subject, const ConcreteAttributePath & path)
-    {
-        // operationFlags is 0 i.e. not internal
-        // readFlags is 0 i.e. not fabric filtered
-        // dataVersion is missing (no data version filtering)
-        request.subjectDescriptor = subject;
-        request.path              = path;
-    }
-
-    std::unique_ptr<AttributeValueEncoder> StartEncoding(DataModel::Provider * model,
-                                                         AttributeEncodeState state = AttributeEncodeState())
-    {
-        std::optional<ClusterInfo> info = model->GetClusterInfo(request.path);
-        if (!info.has_value())
-        {
-            ChipLogError(Test, "Missing cluster information - no data version");
-            return nullptr;
-        }
-
-        DataVersion dataVersion = info->dataVersion; // NOLINT(bugprone-unchecked-optional-access)
-
-        CHIP_ERROR err = encodedIBs.StartEncoding(reportBuilder);
-        if (err != CHIP_NO_ERROR)
-        {
-            ChipLogError(Test, "FAILURE starting encoding %" CHIP_ERROR_FORMAT, err.Format());
-            return nullptr;
-        }
-
-        // TODO: could we test isFabricFiltered and EncodeState?
-
-        // request.subjectDescriptor is known non-null because it is set in the constructor
-        // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
-        return std::make_unique<AttributeValueEncoder>(reportBuilder, *request.subjectDescriptor, request.path, dataVersion,
-                                                       false /* aIsFabricFiltered */, state);
-    }
-
-    CHIP_ERROR FinishEncoding() { return encodedIBs.FinishEncoding(reportBuilder); }
-};
-
-// Sets up data for writing
-struct TestWriteRequest
-{
-    DataModel::WriteAttributeRequest request;
-    uint8_t tlvBuffer[128] = { 0 };
-    TLV::TLVReader
-        tlvReader; /// tlv reader used for the returned AttributeValueDecoder (since attributeValueDecoder uses references)
-
-    TestWriteRequest(const Access::SubjectDescriptor & subject, const ConcreteDataAttributePath & path)
-    {
-        request.subjectDescriptor = subject;
-        request.path              = path;
-    }
-
-    template <typename T>
-    TLV::TLVReader ReadEncodedValue(const T & value)
-    {
-        TLV::TLVWriter writer;
-        writer.Init(tlvBuffer);
-
-        // Encoding is within a structure:
-        //   - BEGIN_STRUCT
-        //     - 1: .....
-        //   - END_STRUCT
-        TLV::TLVType outerContainerType;
-        VerifyOrDie(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, outerContainerType) == CHIP_NO_ERROR);
-        VerifyOrDie(chip::app::DataModel::Encode(writer, TLV::ContextTag(1), value) == CHIP_NO_ERROR);
-        VerifyOrDie(writer.EndContainer(outerContainerType) == CHIP_NO_ERROR);
-        VerifyOrDie(writer.Finalize() == CHIP_NO_ERROR);
-
-        TLV::TLVReader reader;
-        reader.Init(tlvBuffer);
-
-        // position the reader inside the buffer, on the encoded value
-        VerifyOrDie(reader.Next() == CHIP_NO_ERROR);
-        VerifyOrDie(reader.EnterContainer(outerContainerType) == CHIP_NO_ERROR);
-        VerifyOrDie(reader.Next() == CHIP_NO_ERROR);
-
-        return reader;
-    }
-
-    template <class T>
-    AttributeValueDecoder DecoderFor(const T & value)
-    {
-        tlvReader = ReadEncodedValue(value);
-        return AttributeValueDecoder(tlvReader, request.subjectDescriptor.value_or(kDenySubjectDescriptor));
-    }
-};
-
 template <typename T, EmberAfAttributeType ZclType>
 void TestEmberScalarTypeRead(typename NumericAttributeTraits<T>::WorkingType value)
 {
@@ -714,9 +595,8 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestReadRequest testRequest(
-        kAdminSubjectDescriptor,
-        ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZclType)));
+    ReadOperation testRequest(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZclType));
+    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
     // Ember encoding for integers is IDENTICAL to the in-memory representation for them
     typename NumericAttributeTraits<T>::StorageType storage;
@@ -724,17 +604,17 @@
     chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast<const uint8_t *>(&storage), sizeof(storage)));
 
     // Data read via the encoder
-    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
-    ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR);
+    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
     ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);
 
     // Validate after read
     std::vector<DecodedAttributeData> attribute_data;
-    ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR);
+    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
     ASSERT_EQ(attribute_data.size(), 1u);
 
     DecodedAttributeData & encodedData = attribute_data[0];
-    ASSERT_EQ(encodedData.attributePath, testRequest.request.path);
+    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);
 
     typename NumericAttributeTraits<T>::WorkingType actual;
     ASSERT_EQ(chip::app::DataModel::Decode<typename NumericAttributeTraits<T>::WorkingType>(encodedData.dataReader, actual),
@@ -749,9 +629,8 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestReadRequest testRequest(
-        kAdminSubjectDescriptor,
-        ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZclType)));
+    ReadOperation testRequest(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZclType));
+    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
     // Ember encoding for integers is IDENTICAL to the in-memory representation for them
     typename NumericAttributeTraits<T>::StorageType nullValue;
@@ -759,17 +638,17 @@
     chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast<const uint8_t *>(&nullValue), sizeof(nullValue)));
 
     // Data read via the encoder
-    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
-    ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR);
+    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
     ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);
 
     // Validate after read
     std::vector<DecodedAttributeData> attribute_data;
-    ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR);
+    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
     ASSERT_EQ(attribute_data.size(), 1u);
 
     DecodedAttributeData & encodedData = attribute_data[0];
-    ASSERT_EQ(encodedData.attributePath, testRequest.request.path);
+    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);
     chip::app::DataModel::Nullable<typename NumericAttributeTraits<T>::WorkingType> actual;
     ASSERT_EQ(chip::app::DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR);
     ASSERT_TRUE(actual.IsNull());
@@ -784,13 +663,13 @@
 
     // non-nullable test
     {
-        TestWriteRequest test(
-            kAdminSubjectDescriptor,
-            ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZclType)));
+        WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZclType));
+        test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
         AttributeValueDecoder decoder = test.DecoderFor(value);
 
         // write should succeed
-        ASSERT_TRUE(model.WriteAttribute(test.request, decoder).IsSuccess());
+        ASSERT_TRUE(model.WriteAttribute(test.GetRequest(), decoder).IsSuccess());
 
         // Validate data after write
         chip::ByteSpan writtenData = Test::GetEmberBuffer();
@@ -803,7 +682,8 @@
         EXPECT_EQ(actual, value);
         ASSERT_EQ(model.ChangeListener().DirtyList().size(), 1u);
         EXPECT_EQ(model.ChangeListener().DirtyList()[0],
-                  AttributePathParams(test.request.path.mEndpointId, test.request.path.mClusterId, test.request.path.mAttributeId));
+                  AttributePathParams(test.GetRequest().path.mEndpointId, test.GetRequest().path.mClusterId,
+                                      test.GetRequest().path.mAttributeId));
 
         // reset for the next test
         model.ChangeListener().DirtyList().clear();
@@ -811,32 +691,32 @@
 
     // nullable test: write null to make sure content of buffer changed (otherwise it will be a noop for dirty checking)
     {
-        TestWriteRequest test(
-            kAdminSubjectDescriptor,
-            ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZclType)));
+        WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZclType));
+        test.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
         using NumericType             = NumericAttributeTraits<T>;
         using NullableType            = chip::app::DataModel::Nullable<typename NumericType::WorkingType>;
         AttributeValueDecoder decoder = test.DecoderFor<NullableType>(NullableType());
 
         // write should succeed
-        ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR);
+        ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
 
         // dirty: we changed the value to null
         ASSERT_EQ(model.ChangeListener().DirtyList().size(), 1u);
         EXPECT_EQ(model.ChangeListener().DirtyList()[0],
-                  AttributePathParams(test.request.path.mEndpointId, test.request.path.mClusterId, test.request.path.mAttributeId));
+                  AttributePathParams(test.GetRequest().path.mEndpointId, test.GetRequest().path.mClusterId,
+                                      test.GetRequest().path.mAttributeId));
     }
 
     // nullable test
     {
-        TestWriteRequest test(
-            kAdminSubjectDescriptor,
-            ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZclType)));
+        WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZclType));
+        test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
         AttributeValueDecoder decoder = test.DecoderFor(value);
 
         // write should succeed
-        ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR);
+        ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
 
         // Validate data after write
         chip::ByteSpan writtenData = Test::GetEmberBuffer();
@@ -850,7 +730,8 @@
         // dirty a 2nd time when we moved from null to a real value
         ASSERT_EQ(model.ChangeListener().DirtyList().size(), 2u);
         EXPECT_EQ(model.ChangeListener().DirtyList()[1],
-                  AttributePathParams(test.request.path.mEndpointId, test.request.path.mClusterId, test.request.path.mAttributeId));
+                  AttributePathParams(test.GetRequest().path.mEndpointId, test.GetRequest().path.mClusterId,
+                                      test.GetRequest().path.mAttributeId));
     }
 }
 
@@ -861,15 +742,15 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(kAdminSubjectDescriptor,
-                          ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZclType)));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZclType));
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
     using NumericType             = NumericAttributeTraits<T>;
     using NullableType            = chip::app::DataModel::Nullable<typename NumericType::WorkingType>;
     AttributeValueDecoder decoder = test.DecoderFor<NullableType>(NullableType());
 
     // write should succeed
-    ASSERT_TRUE(model.WriteAttribute(test.request, decoder).IsSuccess());
+    ASSERT_TRUE(model.WriteAttribute(test.GetRequest(), decoder).IsSuccess());
 
     // Validate data after write
     chip::ByteSpan writtenData = Test::GetEmberBuffer();
@@ -889,16 +770,15 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(
-        kAdminSubjectDescriptor,
-        ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZclType)));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZclType));
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
     using NumericType             = NumericAttributeTraits<T>;
     using NullableType            = chip::app::DataModel::Nullable<typename NumericType::WorkingType>;
     AttributeValueDecoder decoder = test.DecoderFor<NullableType>(NullableType());
 
     // write should fail: we are trying to write null
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_ERROR_WRONG_TLV_TYPE);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_ERROR_WRONG_TLV_TYPE);
 }
 
 uint16_t ReadLe16(const void * buffer)
@@ -1319,11 +1199,12 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestReadRequest testRequest(kDenySubjectDescriptor,
-                                ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), MockAttributeId(10)));
-    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
+    ReadOperation testRequest(kMockEndpoint1, MockClusterId(1), MockAttributeId(10));
+    testRequest.SetSubjectDescriptor(kDenySubjectDescriptor);
 
-    ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), Status::UnsupportedAccess);
+    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+
+    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), Status::UnsupportedAccess);
 }
 
 TEST(TestCodegenModelViaMocks, ReadForInvalidGlobalAttributePath)
@@ -1333,17 +1214,19 @@
     ScopedMockAccessControl accessControl;
 
     {
-        TestReadRequest testRequest(kAdminSubjectDescriptor,
-                                    ConcreteAttributePath(kEndpointIdThatIsMissing, MockClusterId(1), AttributeList::Id));
-        std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
-        ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), Status::UnsupportedEndpoint);
+        ReadOperation testRequest(kEndpointIdThatIsMissing, MockClusterId(1), AttributeList::Id);
+        testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
+        std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+        ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), Status::UnsupportedEndpoint);
     }
 
     {
-        TestReadRequest testRequest(kAdminSubjectDescriptor,
-                                    ConcreteAttributePath(kMockEndpoint1, kInvalidClusterId, AttributeList::Id));
-        std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
-        ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), Status::UnsupportedCluster);
+        ReadOperation testRequest(kMockEndpoint1, kInvalidClusterId, AttributeList::Id);
+        testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
+        std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+        ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), Status::UnsupportedCluster);
     }
 }
 
@@ -1355,29 +1238,29 @@
 
     // Invalid attribute
     {
-        TestReadRequest testRequest(kAdminSubjectDescriptor,
-                                    ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), MockAttributeId(10)));
-        std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
+        ReadOperation testRequest(kMockEndpoint1, MockClusterId(1), MockAttributeId(10));
+        testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
-        ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), Status::UnsupportedAttribute);
+        std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+        ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), Status::UnsupportedAttribute);
     }
 
     // Invalid cluster
     {
-        TestReadRequest testRequest(kAdminSubjectDescriptor,
-                                    ConcreteAttributePath(kMockEndpoint1, MockClusterId(100), MockAttributeId(1)));
-        std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
+        ReadOperation testRequest(kMockEndpoint1, MockClusterId(100), MockAttributeId(1));
+        testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
+        std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
 
-        ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), Status::UnsupportedCluster);
+        ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), Status::UnsupportedCluster);
     }
 
     // Invalid endpoint
     {
-        TestReadRequest testRequest(kAdminSubjectDescriptor,
-                                    ConcreteAttributePath(kEndpointIdThatIsMissing, MockClusterId(1), MockAttributeId(1)));
-        std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
+        ReadOperation testRequest(kEndpointIdThatIsMissing, MockClusterId(1), MockAttributeId(1));
+        testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
+        std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
 
-        ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), Status::UnsupportedEndpoint);
+        ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), Status::UnsupportedEndpoint);
     }
 }
 
@@ -1387,15 +1270,15 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestReadRequest testRequest(kDenySubjectDescriptor,
-                                ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), MockAttributeId(10)));
-    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
+    ReadOperation testRequest(kMockEndpoint1, MockClusterId(1), MockAttributeId(10));
+    testRequest.SetSubjectDescriptor(kDenySubjectDescriptor);
+    testRequest.SetPathExpanded(true);
 
-    testRequest.request.path.mExpanded = true;
+    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
 
     // For expanded paths, access control failures succeed without encoding anything
     // This is temporary until ACL checks are moved inside the IM/ReportEngine
-    ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR);
+    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
     ASSERT_FALSE(encoder->TriedEncode());
 }
 
@@ -1408,16 +1291,17 @@
     const ConcreteAttributePath kTestPath(kMockEndpoint3, MockClusterId(4),
                                           MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE));
 
-    TestReadRequest testRequest(kAdminSubjectDescriptor, kTestPath);
-    RegisteredAttributeAccessInterface<UnsupportedReadAccessInterface> aai(kTestPath);
+    ReadOperation testRequest(kTestPath);
+    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
+    testRequest.SetPathExpanded(true);
 
-    testRequest.request.path.mExpanded = true;
+    RegisteredAttributeAccessInterface<UnsupportedReadAccessInterface> aai(kTestPath);
 
     // For expanded paths, unsupported read from AAI (i.e. reading write-only data)
     // succeed without attempting to encode.
     // This is temporary until ACL checks are moved inside the IM/ReportEngine
-    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
-    ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR);
+    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
     ASSERT_FALSE(encoder->TriedEncode());
 }
 
@@ -1510,29 +1394,27 @@
     ScopedMockAccessControl accessControl;
 
     {
-        TestReadRequest testRequest(
-            kAdminSubjectDescriptor,
-            ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                                  MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE)));
+        ReadOperation testRequest(kMockEndpoint3, MockClusterId(4),
+                                  MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE));
+        testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
         chip::Test::SetEmberReadOutput(Protocols::InteractionModel::Status::Failure);
 
         // Actual read via an encoder
-        std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
-        ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), Status::Failure);
+        std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+        ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), Status::Failure);
     }
 
     {
-        TestReadRequest testRequest(
-            kAdminSubjectDescriptor,
-            ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                                  MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE)));
+        ReadOperation testRequest(kMockEndpoint3, MockClusterId(4),
+                                  MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE));
+        testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
         chip::Test::SetEmberReadOutput(Protocols::InteractionModel::Status::Busy);
 
         // Actual read via an encoder
-        std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
-        ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), Status::Busy);
+        std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+        ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), Status::Busy);
     }
 
     // reset things to success to not affect other tests
@@ -1545,26 +1427,26 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestReadRequest testRequest(kAdminSubjectDescriptor,
-                                ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                                                      MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE)));
+    ReadOperation testRequest(kMockEndpoint3, MockClusterId(4),
+                              MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE));
+    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
     // NOTE: This is a pascal string of size 0xFFFF which for null strings is a null marker
     char data[] = "\xFF\xFFInvalid length string is null";
     chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast<const uint8_t *>(data), sizeof(data)));
 
     // Actual read via an encoder
-    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
-    ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR);
+    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
     ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);
 
     // Validate after read
     std::vector<DecodedAttributeData> attribute_data;
-    ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR);
+    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
     ASSERT_EQ(attribute_data.size(), 1u);
 
     DecodedAttributeData & encodedData = attribute_data[0];
-    ASSERT_EQ(encodedData.attributePath, testRequest.request.path);
+    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);
 
     // data element should be null for the given 0xFFFF length
     ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Null);
@@ -1580,10 +1462,9 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestReadRequest testRequest(
-        kAdminSubjectDescriptor,
-        ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                              MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE)));
+    ReadOperation testRequest(kMockEndpoint3, MockClusterId(4),
+                              MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE));
+    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
     // NOTE: This is a pascal string, so actual data is "test"
     //       the longer encoding is to make it clear we do not encode the overflow
@@ -1592,17 +1473,17 @@
     chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast<const uint8_t *>(data), sizeof(data)));
 
     // Actual read via an encoder
-    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
-    ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR);
+    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
     ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);
 
     // Validate after read
     std::vector<DecodedAttributeData> attribute_data;
-    ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR);
+    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
     ASSERT_EQ(attribute_data.size(), 1u);
 
     const DecodedAttributeData & encodedData = attribute_data[0];
-    ASSERT_EQ(encodedData.attributePath, testRequest.request.path);
+    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);
 
     // data element should be a encoded byte string as this is what the attribute type is
     ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_ByteString);
@@ -1619,9 +1500,9 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestReadRequest testRequest(kAdminSubjectDescriptor,
-                                ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                                                      MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_OCTET_STRING_ATTRIBUTE_TYPE)));
+    ReadOperation testRequest(kMockEndpoint3, MockClusterId(4),
+                              MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_OCTET_STRING_ATTRIBUTE_TYPE));
+    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
     // NOTE: This is a pascal string, so actual data is "test"
     //       the longer encoding is to make it clear we do not encode the overflow
@@ -1629,17 +1510,17 @@
     chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast<const uint8_t *>(data), sizeof(data)));
 
     // Actual read via an encoder
-    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
-    ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR);
+    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
     ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);
 
     // Validate after read
     std::vector<DecodedAttributeData> attribute_data;
-    ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR);
+    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
     ASSERT_EQ(attribute_data.size(), 1u);
 
     const DecodedAttributeData & encodedData = attribute_data[0];
-    ASSERT_EQ(encodedData.attributePath, testRequest.request.path);
+    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);
 
     // data element should be a encoded byte string as this is what the attribute type is
     ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_ByteString);
@@ -1656,9 +1537,9 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestReadRequest testRequest(kAdminSubjectDescriptor,
-                                ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                                                      MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_CHAR_STRING_ATTRIBUTE_TYPE)));
+    ReadOperation testRequest(kMockEndpoint3, MockClusterId(4),
+                              MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_CHAR_STRING_ATTRIBUTE_TYPE));
+    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
     // NOTE: This is a pascal string, so actual data is "abcde"
     //       the longer encoding is to make it clear we do not encode the overflow
@@ -1667,17 +1548,17 @@
     chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast<const uint8_t *>(data), sizeof(data)));
 
     // Actual read via an encoder
-    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
-    ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR);
+    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
     ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);
 
     // Validate after reading
     std::vector<DecodedAttributeData> attribute_data;
-    ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR);
+    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
     ASSERT_EQ(attribute_data.size(), 1u);
 
     const DecodedAttributeData & encodedData = attribute_data[0];
-    ASSERT_EQ(encodedData.attributePath, testRequest.request.path);
+    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);
 
     // data element should be a encoded byte string as this is what the attribute type is
     ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_UTF8String);
@@ -1692,10 +1573,9 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestReadRequest testRequest(
-        kAdminSubjectDescriptor,
-        ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                              MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE)));
+    ReadOperation testRequest(kMockEndpoint3, MockClusterId(4),
+                              MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE));
+    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
     // NOTE: This is a pascal string, so actual data is "abcde"
     //       the longer encoding is to make it clear we do not encode the overflow
@@ -1704,17 +1584,17 @@
     chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast<const uint8_t *>(data), sizeof(data)));
 
     // Actual read via an encoder
-    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
-    ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR);
+    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
     ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);
 
     // Validate after reading
     std::vector<DecodedAttributeData> attribute_data;
-    ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR);
+    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
     ASSERT_EQ(attribute_data.size(), 1u);
 
     const DecodedAttributeData & encodedData = attribute_data[0];
-    ASSERT_EQ(encodedData.attributePath, testRequest.request.path);
+    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);
 
     // data element should be a encoded byte string as this is what the attribute type is
     ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_UTF8String);
@@ -1732,7 +1612,9 @@
     const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4),
                                             MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE));
 
-    TestReadRequest testRequest(kAdminSubjectDescriptor, kStructPath);
+    ReadOperation testRequest(kStructPath);
+    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
     RegisteredAttributeAccessInterface<StructAttributeAccessInterface> aai(kStructPath);
 
     aai->SetReturnedData(Clusters::UnitTesting::Structs::SimpleStruct::Type{
@@ -1743,17 +1625,17 @@
         .h = 0.125,
     });
 
-    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
-    ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR);
+    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
     ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);
 
     // Validate after read
     std::vector<DecodedAttributeData> attribute_data;
-    ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR);
+    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
     ASSERT_EQ(attribute_data.size(), 1u);
 
     DecodedAttributeData & encodedData = attribute_data[0];
-    ASSERT_EQ(encodedData.attributePath, testRequest.request.path);
+    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);
 
     ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Structure);
     Clusters::UnitTesting::Structs::SimpleStruct::DecodableType actual;
@@ -1775,10 +1657,12 @@
     const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4),
                                             MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE));
 
-    TestReadRequest testRequest(kAdminSubjectDescriptor, kStructPath);
+    ReadOperation testRequest(kStructPath);
+    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
     RegisteredAttributeAccessInterface<ErrorAccessInterface> aai(kStructPath, CHIP_ERROR_KEY_NOT_FOUND);
-    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
-    ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_ERROR_KEY_NOT_FOUND);
+    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_ERROR_KEY_NOT_FOUND);
 }
 
 TEST(TestCodegenModelViaMocks, AttributeAccessInterfaceListRead)
@@ -1790,7 +1674,9 @@
     const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4),
                                             MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_ARRAY_ATTRIBUTE_TYPE));
 
-    TestReadRequest testRequest(kAdminSubjectDescriptor, kStructPath);
+    ReadOperation testRequest(kStructPath);
+    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
     RegisteredAttributeAccessInterface<ListAttributeAcessInterface> aai(kStructPath);
 
     constexpr unsigned kDataCount = 5;
@@ -1802,17 +1688,17 @@
     });
     aai->SetReturnedDataCount(kDataCount);
 
-    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
-    ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR);
+    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
     ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);
 
     // Validate after read
     std::vector<DecodedAttributeData> attribute_data;
-    ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR);
+    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
     ASSERT_EQ(attribute_data.size(), 1u);
 
     DecodedAttributeData & encodedData = attribute_data[0];
-    ASSERT_EQ(encodedData.attributePath, testRequest.request.path);
+    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);
 
     ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Array);
 
@@ -1842,7 +1728,8 @@
     const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4),
                                             MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_ARRAY_ATTRIBUTE_TYPE));
 
-    TestReadRequest testRequest(kAdminSubjectDescriptor, kStructPath);
+    ReadOperation testRequest(kStructPath);
+    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
     RegisteredAttributeAccessInterface<ListAttributeAcessInterface> aai(kStructPath);
 
     constexpr unsigned kDataCount = 1024;
@@ -1854,20 +1741,20 @@
     });
     aai->SetReturnedDataCount(kDataCount);
 
-    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
+    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
     // NOTE: overflow, however data should be valid. Technically both NO_MEMORY and BUFFER_TOO_SMALL
     // should be ok here, however we know buffer-too-small is the error in this case hence
     // the compare (easier to write the test and read the output)
-    ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_ERROR_BUFFER_TOO_SMALL);
+    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_ERROR_BUFFER_TOO_SMALL);
     ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);
 
     // Validate after read
     std::vector<DecodedAttributeData> attribute_data;
-    ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR);
+    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
     ASSERT_EQ(attribute_data.size(), 1u);
 
     DecodedAttributeData & encodedData = attribute_data[0];
-    ASSERT_EQ(encodedData.attributePath, testRequest.request.path);
+    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);
 
     ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Array);
 
@@ -1901,7 +1788,8 @@
     const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4),
                                             MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_ARRAY_ATTRIBUTE_TYPE));
 
-    TestReadRequest testRequest(kAdminSubjectDescriptor, kStructPath);
+    ReadOperation testRequest(kStructPath);
+    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
     RegisteredAttributeAccessInterface<ListAttributeAcessInterface> aai(kStructPath);
 
     constexpr unsigned kDataCount        = 1024;
@@ -1917,16 +1805,18 @@
     AttributeEncodeState encodeState;
     encodeState.SetCurrentEncodingListIndex(kEncodeIndexStart);
 
-    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model, encodeState);
+    std::unique_ptr<AttributeValueEncoder> encoder =
+        testRequest.StartEncoding(ReadOperation::EncodingParams().SetEncodingState(encodeState));
+
     // NOTE: overflow, however data should be valid. Technically both NO_MEMORY and BUFFER_TOO_SMALL
     // should be ok here, however we know buffer-too-small is the error in this case hence
     // the compare (easier to write the test and read the output)
-    ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_ERROR_BUFFER_TOO_SMALL);
+    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_ERROR_BUFFER_TOO_SMALL);
     ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);
 
     // Validate after read
     std::vector<DecodedAttributeData> attribute_data;
-    ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR);
+    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
 
     // Incremental encodes are separate list items, repeated
     // actual size IS ARBITRARY (current test sets it at 11)
@@ -1935,9 +1825,9 @@
     for (unsigned i = 0; i < attribute_data.size(); i++)
     {
         DecodedAttributeData & encodedData = attribute_data[i];
-        ASSERT_EQ(encodedData.attributePath.mEndpointId, testRequest.request.path.mEndpointId);
-        ASSERT_EQ(encodedData.attributePath.mClusterId, testRequest.request.path.mClusterId);
-        ASSERT_EQ(encodedData.attributePath.mAttributeId, testRequest.request.path.mAttributeId);
+        ASSERT_EQ(encodedData.attributePath.mEndpointId, testRequest.GetRequest().path.mEndpointId);
+        ASSERT_EQ(encodedData.attributePath.mClusterId, testRequest.GetRequest().path.mClusterId);
+        ASSERT_EQ(encodedData.attributePath.mAttributeId, testRequest.GetRequest().path.mAttributeId);
         ASSERT_EQ(encodedData.attributePath.mListOp, ConcreteDataAttributePath::ListOperation::AppendItem);
 
         // individual structures encoded in each item
@@ -1960,21 +1850,21 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestReadRequest testRequest(kAdminSubjectDescriptor,
-                                ConcreteAttributePath(kMockEndpoint2, MockClusterId(3), AttributeList::Id));
+    ReadOperation testRequest(kMockEndpoint2, MockClusterId(3), AttributeList::Id);
+    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
     // Data read via the encoder
-    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding(&model);
-    ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_NO_ERROR);
+    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
+    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
     ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);
 
     // Validate after read
     std::vector<DecodedAttributeData> attribute_data;
-    ASSERT_EQ(testRequest.encodedIBs.Decode(attribute_data), CHIP_NO_ERROR);
+    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
     ASSERT_EQ(attribute_data.size(), 1u);
 
     DecodedAttributeData & encodedData = attribute_data[0];
-    ASSERT_EQ(encodedData.attributePath, testRequest.request.path);
+    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);
 
     ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Array);
 
@@ -2019,20 +1909,17 @@
     /* Using this path is also failing existence checks, so this cannot be enabled
      * until we fix ordering of ACL to be done before existence checks
 
-      TestWriteRequest test(kDenySubjectDescriptor,
-                            ConcreteDataAttributePath(kMockEndpoint1, MockClusterId(1), MockAttributeId(10)));
+      WriteOperation test(kMockEndpoint1, MockClusterId(1), MockAttributeId(10));
       AttributeValueDecoder decoder = test.DecoderFor<uint32_t>(1234);
 
-      ASSERT_EQ(model.WriteAttribute(test.request, decoder), Status::UnsupportedAccess);
+      ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::UnsupportedAccess);
       ASSERT_TRUE(model.ChangeListener().DirtyList().empty());
     */
 
-    TestWriteRequest test(kDenySubjectDescriptor,
-                          ConcreteDataAttributePath(kMockEndpoint3, MockClusterId(4),
-                                                    MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_INT32U_ATTRIBUTE_TYPE)));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_INT32U_ATTRIBUTE_TYPE));
     AttributeValueDecoder decoder = test.DecoderFor<uint32_t>(1234);
 
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), Status::UnsupportedAccess);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::UnsupportedAccess);
     ASSERT_TRUE(model.ChangeListener().DirtyList().empty());
 }
 
@@ -2094,16 +1981,15 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(
-        kAdminSubjectDescriptor,
-        ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_INT32U_ATTRIBUTE_TYPE)));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_INT32U_ATTRIBUTE_TYPE));
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
     using NumericType             = NumericAttributeTraits<uint32_t>;
     using NullableType            = chip::app::DataModel::Nullable<typename NumericType::WorkingType>;
     AttributeValueDecoder decoder = test.DecoderFor<NullableType>(0xFFFFFFFF);
 
     // write should fail: we are trying to write null which is out of range
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), Status::ConstraintError);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::ConstraintError);
 }
 
 TEST(TestCodegenModelViaMocks, EmberTestWriteOutOfRepresentableRangeOddIntegerNonNullable)
@@ -2112,9 +1998,8 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(kAdminSubjectDescriptor,
-                          ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                                                MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_INT24U_ATTRIBUTE_TYPE)));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_INT24U_ATTRIBUTE_TYPE));
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
     using NumericType             = NumericAttributeTraits<uint32_t>;
     using NullableType            = chip::app::DataModel::Nullable<typename NumericType::WorkingType>;
@@ -2122,7 +2007,7 @@
 
     // write should fail: written value is not in range
     // NOTE: this matches legacy behaviour, however realistically maybe ConstraintError would be more correct
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_ERROR_INVALID_ARGUMENT);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_ERROR_INVALID_ARGUMENT);
 }
 
 TEST(TestCodegenModelViaMocks, EmberTestWriteOutOfRepresentableRangeOddIntegerNullable)
@@ -2131,9 +2016,8 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(
-        kAdminSubjectDescriptor,
-        ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_INT24U_ATTRIBUTE_TYPE)));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_INT24U_ATTRIBUTE_TYPE));
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
     using NumericType             = NumericAttributeTraits<uint32_t>;
     using NullableType            = chip::app::DataModel::Nullable<typename NumericType::WorkingType>;
@@ -2141,7 +2025,7 @@
 
     // write should fail: written value is not in range
     // NOTE: this matches legacy behaviour, however realistically maybe ConstraintError would be more correct
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_ERROR_INVALID_ARGUMENT);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_ERROR_INVALID_ARGUMENT);
 }
 
 TEST(TestCodegenModelViaMoceNullValueToNullables, EmberAttributeWriteBasicTypesLowestValue)
@@ -2186,12 +2070,12 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(kAdminSubjectDescriptor,
-                          ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                                                MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_CHAR_STRING_ATTRIBUTE_TYPE)));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_CHAR_STRING_ATTRIBUTE_TYPE));
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
     AttributeValueDecoder decoder = test.DecoderFor<CharSpan>("hello world"_span);
 
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
     chip::ByteSpan writtenData = GetEmberBuffer();
     chip::CharSpan asCharSpan(reinterpret_cast<const char *>(writtenData.data()), writtenData[0] + 1);
     ASSERT_TRUE(asCharSpan.data_equal("\x0Bhello world"_span));
@@ -2203,15 +2087,15 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(kAdminSubjectDescriptor,
-                          ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                                                MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE)));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4),
+                        MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE));
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
     // Mocks allow for 16 bytes only by default for string attributes
     AttributeValueDecoder decoder = test.DecoderFor<CharSpan>(
         "this is a very long string that will be longer than the default attribute size for our mocks"_span);
 
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), Status::InvalidValue);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::InvalidValue);
 }
 
 TEST(TestCodegenModelViaMocks, EmberAttributeWriteLongString)
@@ -2220,12 +2104,13 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(kAdminSubjectDescriptor,
-                          ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                                                MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE)));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4),
+                        MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE));
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
     AttributeValueDecoder decoder = test.DecoderFor<CharSpan>("text"_span);
 
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
     chip::ByteSpan writtenData = GetEmberBuffer();
 
     uint16_t len = ReadLe16(writtenData.data());
@@ -2241,13 +2126,13 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(kAdminSubjectDescriptor,
-                          ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                                                MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE)));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE));
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
     AttributeValueDecoder decoder =
         test.DecoderFor<chip::app::DataModel::Nullable<CharSpan>>(chip::app::DataModel::MakeNullable("text"_span));
 
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
     chip::ByteSpan writtenData = GetEmberBuffer();
 
     uint16_t len = ReadLe16(writtenData.data());
@@ -2263,13 +2148,13 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(kAdminSubjectDescriptor,
-                          ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                                                MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE)));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE));
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
     AttributeValueDecoder decoder =
         test.DecoderFor<chip::app::DataModel::Nullable<CharSpan>>(chip::app::DataModel::Nullable<CharSpan>());
 
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
     chip::ByteSpan writtenData = GetEmberBuffer();
     ASSERT_EQ(writtenData[0], 0xFF);
     ASSERT_EQ(writtenData[1], 0xFF);
@@ -2281,14 +2166,14 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(kAdminSubjectDescriptor,
-                          ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                                                MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_OCTET_STRING_ATTRIBUTE_TYPE)));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_OCTET_STRING_ATTRIBUTE_TYPE));
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
     uint8_t buffer[] = { 11, 12, 13 };
 
     AttributeValueDecoder decoder = test.DecoderFor<ByteSpan>(ByteSpan(buffer));
 
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
     chip::ByteSpan writtenData = GetEmberBuffer();
 
     EXPECT_EQ(writtenData[0], 3u);
@@ -2303,14 +2188,15 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(kAdminSubjectDescriptor,
-                          ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                                                MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE)));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4),
+                        MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE));
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
     uint8_t buffer[] = { 11, 12, 13 };
 
     AttributeValueDecoder decoder = test.DecoderFor<ByteSpan>(ByteSpan(buffer));
 
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
     chip::ByteSpan writtenData = GetEmberBuffer();
 
     uint16_t len = ReadLe16(writtenData.data());
@@ -2327,14 +2213,16 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(kAdminSubjectDescriptor, ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), kAttributeIdTimedWrite));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4), kAttributeIdTimedWrite);
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
     AttributeValueDecoder decoder = test.DecoderFor<int32_t>(1234);
 
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), Status::NeedsTimedInteraction);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::NeedsTimedInteraction);
 
     // writing as timed should be fine
-    test.request.writeFlags.Set(WriteFlags::kTimed);
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR);
+    test.SetWriteFlags(WriteFlags::kTimed);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
 }
 
 TEST(TestCodegenModelViaMocks, EmberAttributeWriteReadOnlyAttribute)
@@ -2343,14 +2231,16 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(kAdminSubjectDescriptor, ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), kAttributeIdReadOnly));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4), kAttributeIdReadOnly);
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
     AttributeValueDecoder decoder = test.DecoderFor<int32_t>(1234);
 
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), Status::UnsupportedWrite);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::UnsupportedWrite);
 
     // Internal writes bypass the read only requirement
-    test.request.operationFlags.Set(OperationFlags::kInternal);
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR);
+    test.SetOperationFlags(OperationFlags::kInternal);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
 }
 
 TEST(TestCodegenModelViaMocks, EmberAttributeWriteDataVersion)
@@ -2359,25 +2249,24 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(kAdminSubjectDescriptor,
-                          ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                                                MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_INT32S_ATTRIBUTE_TYPE)));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_INT32S_ATTRIBUTE_TYPE));
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
     // Initialize to some version
     ResetVersion();
     BumpVersion();
-    test.request.path.mDataVersion = MakeOptional(GetVersion());
+    test.SetDataVersion(MakeOptional(GetVersion()));
 
     // Make version invalid
     BumpVersion();
 
     AttributeValueDecoder decoder = test.DecoderFor<int32_t>(1234);
 
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), Status::DataVersionMismatch);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::DataVersionMismatch);
 
     // Write passes if we set the right version for the data
-    test.request.path.mDataVersion = MakeOptional(GetVersion());
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR);
+    test.SetDataVersion(MakeOptional(GetVersion()));
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
 }
 
 TEST(TestCodegenModelViaMocks, WriteToInvalidPath)
@@ -2387,20 +2276,26 @@
     ScopedMockAccessControl accessControl;
 
     {
-        TestWriteRequest test(kAdminSubjectDescriptor, ConcreteAttributePath(kInvalidEndpointId, MockClusterId(1234), 1234));
+        WriteOperation test(kInvalidEndpointId, MockClusterId(1234), 1234);
+        test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
         AttributeValueDecoder decoder = test.DecoderFor<int32_t>(1234);
-        ASSERT_EQ(model.WriteAttribute(test.request, decoder), Status::UnsupportedEndpoint);
+        ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::UnsupportedEndpoint);
     }
     {
-        TestWriteRequest test(kAdminSubjectDescriptor, ConcreteAttributePath(kMockEndpoint1, MockClusterId(1234), 1234));
+        WriteOperation test(kMockEndpoint1, MockClusterId(1234), 1234);
+        test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
         AttributeValueDecoder decoder = test.DecoderFor<int32_t>(1234);
-        ASSERT_EQ(model.WriteAttribute(test.request, decoder), Status::UnsupportedCluster);
+        ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::UnsupportedCluster);
     }
 
     {
-        TestWriteRequest test(kAdminSubjectDescriptor, ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), 1234));
+        WriteOperation test(kMockEndpoint1, MockClusterId(1), 1234);
+        test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
         AttributeValueDecoder decoder = test.DecoderFor<int32_t>(1234);
-        ASSERT_EQ(model.WriteAttribute(test.request, decoder), Status::UnsupportedAttribute);
+        ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::UnsupportedAttribute);
     }
 }
 
@@ -2410,9 +2305,11 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(kAdminSubjectDescriptor, ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), AttributeList::Id));
+    WriteOperation test(kMockEndpoint1, MockClusterId(1), AttributeList::Id);
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
     AttributeValueDecoder decoder = test.DecoderFor<int32_t>(1234);
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), Status::UnsupportedWrite);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::UnsupportedWrite);
 }
 
 TEST(TestCodegenModelViaMocks, EmberWriteFailure)
@@ -2421,19 +2318,18 @@
     CodegenDataModelProviderWithContext model;
     ScopedMockAccessControl accessControl;
 
-    TestWriteRequest test(kAdminSubjectDescriptor,
-                          ConcreteAttributePath(kMockEndpoint3, MockClusterId(4),
-                                                MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_INT32S_ATTRIBUTE_TYPE)));
+    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_INT32S_ATTRIBUTE_TYPE));
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
 
     {
         AttributeValueDecoder decoder = test.DecoderFor<int32_t>(1234);
         chip::Test::SetEmberReadOutput(Protocols::InteractionModel::Status::Failure);
-        ASSERT_EQ(model.WriteAttribute(test.request, decoder), Status::Failure);
+        ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::Failure);
     }
     {
         AttributeValueDecoder decoder = test.DecoderFor<int32_t>(1234);
         chip::Test::SetEmberReadOutput(Protocols::InteractionModel::Status::Busy);
-        ASSERT_EQ(model.WriteAttribute(test.request, decoder), Status::Busy);
+        ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::Busy);
     }
     // reset things to success to not affect other tests
     chip::Test::SetEmberReadOutput(ByteSpan());
@@ -2449,7 +2345,9 @@
                                             MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE));
     RegisteredAttributeAccessInterface<StructAttributeAccessInterface> aai(kStructPath);
 
-    TestWriteRequest test(kAdminSubjectDescriptor, kStructPath);
+    WriteOperation test(kStructPath);
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
     Clusters::UnitTesting::Structs::SimpleStruct::Type testValue{
         .a = 112,
         .b = true,
@@ -2459,7 +2357,7 @@
     };
 
     AttributeValueDecoder decoder = test.DecoderFor(testValue);
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
 
     EXPECT_EQ(aai->GetData().a, 112);
     EXPECT_TRUE(aai->GetData().e.data_equal("aai_write_test"_span));
@@ -2527,7 +2425,9 @@
                                             MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE));
     RegisteredAttributeAccessInterface<ErrorAccessInterface> aai(kStructPath, CHIP_ERROR_KEY_NOT_FOUND);
 
-    TestWriteRequest test(kAdminSubjectDescriptor, kStructPath);
+    WriteOperation test(kStructPath);
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
     Clusters::UnitTesting::Structs::SimpleStruct::Type testValue{
         .a = 112,
         .b = true,
@@ -2537,7 +2437,7 @@
     };
 
     AttributeValueDecoder decoder = test.DecoderFor(testValue);
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_ERROR_KEY_NOT_FOUND);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_ERROR_KEY_NOT_FOUND);
     ASSERT_TRUE(model.ChangeListener().DirtyList().empty());
 }
 
@@ -2550,7 +2450,9 @@
     const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4),
                                             MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE));
 
-    TestWriteRequest test(kAdminSubjectDescriptor, kStructPath);
+    WriteOperation test(kStructPath);
+    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
+
     Clusters::UnitTesting::Structs::SimpleStruct::Type testValue{
         .a = 112,
         .b = true,
@@ -2563,6 +2465,6 @@
 
     // Embed specifically DOES NOT support structures.
     // Without AAI, we expect a data type error (translated to failure)
-    ASSERT_EQ(model.WriteAttribute(test.request, decoder), Status::Failure);
+    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::Failure);
     ASSERT_TRUE(model.ChangeListener().DirtyList().empty());
 }
diff --git a/src/app/data-model-provider/tests/BUILD.gn b/src/app/data-model-provider/tests/BUILD.gn
index 9829a2f..006976d 100644
--- a/src/app/data-model-provider/tests/BUILD.gn
+++ b/src/app/data-model-provider/tests/BUILD.gn
@@ -30,3 +30,30 @@
     "${chip_root}/src/lib/core:string-builder-adapters",
   ]
 }
+
+source_set("encode-decode") {
+  # Handles the conversions between Raw data (usually byte buffers backed by TLV encoders
+  # and `*IBs::Builder` classes) and data used by by Providers like AttributeValueEncoder
+  # or AttributeValueDecoder.
+  #
+  # Intended to make writing tests simpler and focusing less on encoding and more on
+  # "Assume you receive an int" or "Check that the encoded value was the string 'abc'"
+  # and so on
+  sources = [
+    "ReadTesting.cpp",
+    "ReadTesting.h",
+    "TestConstants.h",
+    "WriteTesting.cpp",
+    "WriteTesting.h",
+  ]
+
+  public_deps = [
+    "${chip_root}/src/access:types",
+    "${chip_root}/src/app:attribute-access",
+    "${chip_root}/src/app:paths",
+    "${chip_root}/src/app/MessageDef",
+    "${chip_root}/src/app/data-model-provider",
+    "${chip_root}/src/lib/core",
+    "${chip_root}/src/lib/core:types",
+  ]
+}
diff --git a/src/app/codegen-data-model-provider/tests/AttributeReportIBEncodeDecode.cpp b/src/app/data-model-provider/tests/ReadTesting.cpp
similarity index 74%
rename from src/app/codegen-data-model-provider/tests/AttributeReportIBEncodeDecode.cpp
rename to src/app/data-model-provider/tests/ReadTesting.cpp
index 29088d0..b7b34c5 100644
--- a/src/app/codegen-data-model-provider/tests/AttributeReportIBEncodeDecode.cpp
+++ b/src/app/data-model-provider/tests/ReadTesting.cpp
@@ -14,27 +14,14 @@
  *    See the License for the specific language governing permissions and
  *    limitations under the License.
  */
-#include "AttributeReportIBEncodeDecode.h"
+#include <app/data-model-provider/tests/ReadTesting.h>
 
-#include <app/MessageDef/AttributePathIB.h>
-#include <app/MessageDef/AttributeReportIB.h>
-
-using namespace chip::app;
+#include <app/MessageDef/ReportDataMessage.h>
 
 namespace chip {
-namespace Test {
-
-CHIP_ERROR DecodedAttributeData::DecodeFrom(const AttributeDataIB::Parser & parser)
-{
-    ReturnErrorOnFailure(parser.GetDataVersion(&dataVersion));
-
-    AttributePathIB::Parser pathParser;
-    ReturnErrorOnFailure(parser.GetPath(&pathParser));
-    ReturnErrorOnFailure(pathParser.GetConcreteAttributePath(attributePath, AttributePathIB::ValidateIdRanges::kNo));
-    ReturnErrorOnFailure(parser.GetData(&dataReader));
-
-    return CHIP_NO_ERROR;
-}
+namespace app {
+namespace Testing {
+namespace {
 
 CHIP_ERROR DecodeAttributeReportIBs(ByteSpan data, std::vector<DecodedAttributeData> & decoded_items)
 {
@@ -104,6 +91,46 @@
     return err;
 }
 
+} // namespace
+
+std::unique_ptr<AttributeValueEncoder> ReadOperation::StartEncoding(const EncodingParams & params)
+{
+    VerifyOrDie((mState == State::kEncoding) || (mState == State::kInitializing));
+    mState = State::kEncoding;
+
+    CHIP_ERROR err = mEncodedIBs.StartEncoding(mAttributeReportIBsBuilder);
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogError(Test, "FAILURE starting encoding %" CHIP_ERROR_FORMAT, err.Format());
+        return nullptr;
+    }
+
+    // mRequest.subjectDescriptor is known non-null because it is set in the constructor
+    // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
+    return std::make_unique<AttributeValueEncoder>(mAttributeReportIBsBuilder, *mRequest.subjectDescriptor, mRequest.path,
+                                                   params.GetDataVersion(), params.GetIsFabricFiltered(),
+                                                   params.GetAttributeEncodeState());
+}
+
+CHIP_ERROR ReadOperation::FinishEncoding()
+{
+    VerifyOrDie(mState == State::kEncoding);
+    mState = State::kFinished;
+    return mEncodedIBs.FinishEncoding(mAttributeReportIBsBuilder);
+}
+
+CHIP_ERROR DecodedAttributeData::DecodeFrom(const AttributeDataIB::Parser & parser)
+{
+    ReturnErrorOnFailure(parser.GetDataVersion(&dataVersion));
+
+    AttributePathIB::Parser pathParser;
+    ReturnErrorOnFailure(parser.GetPath(&pathParser));
+    ReturnErrorOnFailure(pathParser.GetConcreteAttributePath(attributePath, AttributePathIB::ValidateIdRanges::kNo));
+    ReturnErrorOnFailure(parser.GetData(&dataReader));
+
+    return CHIP_NO_ERROR;
+}
+
 CHIP_ERROR EncodedReportIBs::StartEncoding(app::AttributeReportIBs::Builder & builder)
 {
     mEncodeWriter.Init(mTlvDataBuffer);
@@ -121,10 +148,11 @@
     return CHIP_NO_ERROR;
 }
 
-CHIP_ERROR EncodedReportIBs::Decode(std::vector<DecodedAttributeData> & decoded_items)
+CHIP_ERROR EncodedReportIBs::Decode(std::vector<DecodedAttributeData> & decoded_items) const
 {
     return DecodeAttributeReportIBs(mDecodeSpan, decoded_items);
 }
 
-} // namespace Test
+} // namespace Testing
+} // namespace app
 } // namespace chip
diff --git a/src/app/data-model-provider/tests/ReadTesting.h b/src/app/data-model-provider/tests/ReadTesting.h
new file mode 100644
index 0000000..f7cee20
--- /dev/null
+++ b/src/app/data-model-provider/tests/ReadTesting.h
@@ -0,0 +1,217 @@
+/*
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+#pragma once
+
+#include <access/SubjectDescriptor.h>
+#include <app/AttributeEncodeState.h>
+#include <app/AttributeValueEncoder.h>
+#include <app/ConcreteAttributePath.h>
+#include <app/data-model-provider/OperationTypes.h>
+#include <app/data-model-provider/tests/TestConstants.h>
+#include <lib/core/DataModelTypes.h>
+#include <lib/support/BitFlags.h>
+
+#include <vector>
+
+namespace chip {
+namespace app {
+namespace Testing {
+
+/// Contains information about a single parsed item inside an attribute data IB
+struct DecodedAttributeData
+{
+    chip::DataVersion dataVersion;
+    chip::app::ConcreteDataAttributePath attributePath;
+    chip::TLV::TLVReader dataReader;
+
+    CHIP_ERROR DecodeFrom(const chip::app::AttributeDataIB::Parser & parser);
+};
+
+/// Maintains an internal TLV buffer for data encoding and
+/// decoding for ReportIBs.
+///
+/// Main use case is that explicit TLV layouts (structure and container starting) need to be
+/// prepared to have a proper AttributeReportIBs::Builder/parser to exist.
+class EncodedReportIBs
+{
+public:
+    /// Initialize the report structures required to encode a
+    CHIP_ERROR StartEncoding(app::AttributeReportIBs::Builder & builder);
+    CHIP_ERROR FinishEncoding(app::AttributeReportIBs::Builder & builder);
+
+    /// Decode the embedded attribute report IBs.
+    /// The TLVReaders inside data have a lifetime tied to the current object (its readers point
+    /// inside the current object)
+    CHIP_ERROR Decode(std::vector<DecodedAttributeData> & decoded_items) const;
+
+private:
+    constexpr static size_t kMaxTLVBufferSize = 1024;
+
+    uint8_t mTlvDataBuffer[kMaxTLVBufferSize];
+    TLV::TLVType mOuterStructureType;
+    TLV::TLVWriter mEncodeWriter;
+    ByteSpan mDecodeSpan;
+};
+
+/// Contains a `ReadAttributeRequest` as well as classes to convert this into a AttributeReportIBs
+/// and later decode it
+///
+/// It wraps boilerplate code to obtain a `AttributeValueEncoder` as well as later decoding
+/// the underlying encoded data for verification.
+///
+/// Usage:
+///
+///    ReadOperation operation(1 /* endpoint */, 2 /* cluster */, 3 /* attribute */);
+///
+///    auto encoder = operation.StartEncoding(/* ... */);
+///    ASSERT_NE(encoder.get(), nullptr);
+///
+///    // use encoder, like pass in to a WriteAttribute() call
+///
+///    ASSERT_EQ(operation.FinishEncoding(), CHIP_NO_ERROR);
+///
+///    // extract the written data
+///    std::vector<DecodedAttributeData> items;
+///    ASSERT_EQ(read_request.GetEncodedIBs().Decode(items), CHIP_NO_ERROR);
+///
+///    // can use items::dataReader for a chip::TLV::TLVReader access to the underlying data
+///    // for example
+///    uint32_t value = 0;
+///    chip::TLV::TLVReader reader(items[0].dataReader);
+///    ASSERT_EQ(reader.Get(value), CHIP_NO_ERROR);
+///    ASSERT_EQ(value, 123u);
+///
+///
+class ReadOperation
+{
+public:
+    /// Represents parameters for StartEncoding
+    class EncodingParams
+    {
+    public:
+        EncodingParams() {}
+
+        EncodingParams & SetDataVersion(chip::DataVersion v)
+        {
+            mDataVersion = v;
+            return *this;
+        }
+
+        EncodingParams & SetIsFabricFiltered(bool filtered)
+        {
+            mIsFabricFiltered = filtered;
+            return *this;
+        }
+
+        EncodingParams & SetEncodingState(const AttributeEncodeState & state)
+        {
+            mAttributeEncodeState = state;
+            return *this;
+        }
+
+        chip::DataVersion GetDataVersion() const { return mDataVersion; }
+        bool GetIsFabricFiltered() const { return mIsFabricFiltered; }
+        const AttributeEncodeState & GetAttributeEncodeState() const { return mAttributeEncodeState; }
+
+    private:
+        chip::DataVersion mDataVersion = 0x1234;
+        bool mIsFabricFiltered         = false;
+        AttributeEncodeState mAttributeEncodeState;
+    };
+
+    ReadOperation(const ConcreteAttributePath & path)
+    {
+        mRequest.path              = path;
+        mRequest.subjectDescriptor = kDenySubjectDescriptor;
+    }
+
+    ReadOperation(EndpointId endpoint, ClusterId cluster, AttributeId attribute) :
+        ReadOperation(ConcreteAttributePath(endpoint, cluster, attribute))
+    {}
+
+    ReadOperation & SetSubjectDescriptor(const chip::Access::SubjectDescriptor & descriptor)
+    {
+        VerifyOrDie(mState == State::kInitializing);
+        mRequest.subjectDescriptor = descriptor;
+        return *this;
+    }
+
+    ReadOperation & SetReadFlags(const BitFlags<DataModel::ReadFlags> & flags)
+    {
+        VerifyOrDie(mState == State::kInitializing);
+        mRequest.readFlags = flags;
+        return *this;
+    }
+
+    ReadOperation & SetOperationFlags(const BitFlags<DataModel::OperationFlags> & flags)
+    {
+        VerifyOrDie(mState == State::kInitializing);
+        mRequest.operationFlags = flags;
+        return *this;
+    }
+
+    ReadOperation & SetPathExpanded(bool value)
+    {
+        VerifyOrDie(mState == State::kInitializing);
+        mRequest.path.mExpanded = value;
+        return *this;
+    }
+
+    /// Start the encoding of a new element with the given data version associated to it.
+    ///
+    /// The input attribute encoding state will be attached to the returned value encoded (so that
+    /// encoding for list elements is possible)
+    ///
+    std::unique_ptr<AttributeValueEncoder> StartEncoding(const EncodingParams & params = EncodingParams());
+
+    /// Completes the encoding and finalizes the undelying AttributeReport.
+    ///
+    /// Call this to finish a set of `StartEncoding` values and have access to
+    /// the underlying `GetEncodedIBs`
+    CHIP_ERROR FinishEncoding();
+
+    /// Get the underlying read request (i.e. path and flags) for this request
+    const DataModel::ReadAttributeRequest & GetRequest() const { return mRequest; }
+
+    /// Once encoding has finished, you can get access to the underlying
+    /// written data via GetEncodedIBs.
+    const EncodedReportIBs & GetEncodedIBs() const
+    {
+        VerifyOrDie(mState == State::kFinished);
+        return mEncodedIBs;
+    }
+
+private:
+    enum class State
+    {
+        kInitializing, // Setting up initial values (i.e. setting up mRequest)
+        kEncoding,     // Encoding values via StartEncoding
+        kFinished,     // FinishEncoding has been called
+
+    };
+    State mState = State::kInitializing;
+
+    DataModel::ReadAttributeRequest mRequest;
+
+    // encoded-used classes
+    EncodedReportIBs mEncodedIBs;
+    AttributeReportIBs::Builder mAttributeReportIBsBuilder;
+};
+
+} // namespace Testing
+} // namespace app
+} // namespace chip
diff --git a/src/app/data-model-provider/tests/TestConstants.h b/src/app/data-model-provider/tests/TestConstants.h
new file mode 100644
index 0000000..8d62736
--- /dev/null
+++ b/src/app/data-model-provider/tests/TestConstants.h
@@ -0,0 +1,49 @@
+/*
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+#pragma once
+
+#include <access/SubjectDescriptor.h>
+#include <lib/core/DataModelTypes.h>
+
+namespace chip {
+namespace app {
+namespace Testing {
+
+constexpr FabricIndex kTestFabrixIndex = kMinValidFabricIndex;
+constexpr NodeId kTestNodeId           = 0xFFFF'1234'ABCD'4321;
+
+constexpr Access::SubjectDescriptor kAdminSubjectDescriptor{
+    .fabricIndex = kTestFabrixIndex,
+    .authMode    = Access::AuthMode::kCase,
+    .subject     = kTestNodeId,
+};
+
+constexpr Access::SubjectDescriptor kViewSubjectDescriptor{
+    .fabricIndex = kTestFabrixIndex + 1,
+    .authMode    = Access::AuthMode::kCase,
+    .subject     = kTestNodeId,
+};
+
+constexpr Access::SubjectDescriptor kDenySubjectDescriptor{
+    .fabricIndex = kTestFabrixIndex + 2,
+    .authMode    = Access::AuthMode::kCase,
+    .subject     = kTestNodeId,
+};
+
+} // namespace Testing
+} // namespace app
+} // namespace chip
diff --git a/src/app/data-model-provider/tests/WriteTesting.cpp b/src/app/data-model-provider/tests/WriteTesting.cpp
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/app/data-model-provider/tests/WriteTesting.cpp
diff --git a/src/app/data-model-provider/tests/WriteTesting.h b/src/app/data-model-provider/tests/WriteTesting.h
new file mode 100644
index 0000000..d18fb93
--- /dev/null
+++ b/src/app/data-model-provider/tests/WriteTesting.h
@@ -0,0 +1,143 @@
+/*
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+#pragma once
+
+#include "lib/core/DataModelTypes.h"
+#include <app/AttributeValueDecoder.h>
+#include <app/data-model-provider/OperationTypes.h>
+#include <app/data-model-provider/tests/TestConstants.h>
+#include <app/data-model/Encode.h>
+#include <lib/core/TLVReader.h>
+
+namespace chip {
+namespace app {
+namespace Testing {
+
+/// Contains support for setting up a WriteAttributeRequest and underlying data.
+///
+/// It wraps the boilerplate to obtain a AttributeValueDecoder that can be passed in
+/// to DataModel::Provider calls.
+///
+/// Usage:
+///
+///    WriteOperation operation(1 /* endpoint */, 2 /* cluster */, 3 /* attribute */);
+///    test.SetSubjectDescriptor(kAdminSubjectDescriptor);  // optional set access
+///
+///    AttributeValueDecoder decoder = test.DecoderFor<uint32_t>(0x1234);
+///
+///    // decoder is usable at this point
+///    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
+class WriteOperation
+{
+public:
+    WriteOperation(const ConcreteDataAttributePath & path)
+    {
+        mRequest.path              = path;
+        mRequest.subjectDescriptor = kDenySubjectDescriptor;
+    }
+
+    WriteOperation(EndpointId endpoint, ClusterId cluster, AttributeId attribute) :
+        WriteOperation(ConcreteAttributePath(endpoint, cluster, attribute))
+    {}
+
+    WriteOperation & SetSubjectDescriptor(const chip::Access::SubjectDescriptor & descriptor)
+    {
+        mRequest.subjectDescriptor = descriptor;
+        return *this;
+    }
+
+    WriteOperation & SetPreviousSuccessPath(std::optional<ConcreteAttributePath> path)
+    {
+        mRequest.previousSuccessPath = path;
+        return *this;
+    }
+
+    WriteOperation & SetDataVersion(Optional<DataVersion> version)
+    {
+        mRequest.path.mDataVersion = version;
+        return *this;
+    }
+
+    WriteOperation & SetWriteFlags(const BitFlags<DataModel::WriteFlags> & flags)
+    {
+        mRequest.writeFlags = flags;
+        return *this;
+    }
+
+    WriteOperation & SetOperationFlags(const BitFlags<DataModel::OperationFlags> & flags)
+    {
+        mRequest.operationFlags = flags;
+        return *this;
+    }
+
+    WriteOperation & SetPathExpanded(bool value)
+    {
+        mRequest.path.mExpanded = value;
+        return *this;
+    }
+
+    const DataModel::WriteAttributeRequest & GetRequest() const { return mRequest; }
+
+    template <typename T>
+    TLV::TLVReader ReadEncodedValue(const T & value)
+    {
+        TLV::TLVWriter writer;
+        writer.Init(mTLVBuffer);
+
+        // Encoding is within a structure:
+        //   - BEGIN_STRUCT
+        //     - 1: .....
+        //   - END_STRUCT
+        TLV::TLVType outerContainerType;
+        VerifyOrDie(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, outerContainerType) == CHIP_NO_ERROR);
+        VerifyOrDie(chip::app::DataModel::Encode(writer, TLV::ContextTag(1), value) == CHIP_NO_ERROR);
+        VerifyOrDie(writer.EndContainer(outerContainerType) == CHIP_NO_ERROR);
+        VerifyOrDie(writer.Finalize() == CHIP_NO_ERROR);
+
+        TLV::TLVReader reader;
+        reader.Init(mTLVBuffer);
+
+        // position the reader inside the buffer, on the encoded value
+        VerifyOrDie(reader.Next() == CHIP_NO_ERROR);
+        VerifyOrDie(reader.EnterContainer(outerContainerType) == CHIP_NO_ERROR);
+        VerifyOrDie(reader.Next() == CHIP_NO_ERROR);
+
+        return reader;
+    }
+
+    template <class T>
+    AttributeValueDecoder DecoderFor(const T & value)
+    {
+        mTLVReader = ReadEncodedValue(value);
+        return AttributeValueDecoder(mTLVReader, mRequest.subjectDescriptor.value_or(kDenySubjectDescriptor));
+    }
+
+private:
+    constexpr static size_t kMaxTLVBufferSize = 1024;
+
+    DataModel::WriteAttributeRequest mRequest;
+
+    // where data is being written
+    uint8_t mTLVBuffer[kMaxTLVBufferSize] = { 0 };
+
+    // tlv reader used for the returned AttributeValueDecoder (since attributeValueDecoder uses references)
+    TLV::TLVReader mTLVReader;
+};
+
+} // namespace Testing
+} // namespace app
+} // namespace chip