77import static org .testng .Assert .assertTrue ;
88
99import com .linkedin .common .urn .Urn ;
10- import com .linkedin .common .urn .UrnUtils ;
1110import com .linkedin .data .template .RecordTemplate ;
1211import com .linkedin .events .metadata .ChangeType ;
1312import com .linkedin .metadata .aspect .AspectRetriever ;
2221import java .util .List ;
2322import java .util .stream .Collectors ;
2423import javax .annotation .Nonnull ;
24+ import org .testng .SkipException ;
2525import org .testng .annotations .BeforeMethod ;
2626import org .testng .annotations .Test ;
2727
3131 * on supplying realistic aspect payloads and asserting the correctness of the transformed result.
3232 *
3333 * <p>Extend this class for every concrete {@link AspectMigrationMutator} implementation. All
34- * contract-level {@code @Test} methods are inherited automatically — the subclass only needs to
35- * supply three things :
34+ * contract-level {@code @Test} methods are inherited automatically. The subclass must implement
35+ * four abstract methods and may optionally override one more :
3636 *
3737 * <ol>
38- * <li>{@link #mutator()} — the implementation under test
39- * <li>{@link #provideSourceAspect()} — a realistic aspect payload at {@code sourceVersion}
40- * <li>{@link #assertTransformed(RecordTemplate)} — assertions specific to the migration logic
38+ * <li><b>Required:</b> {@link #mutator()} — the implementation under test
39+ * <li><b>Required:</b> {@link #entityUrn()} — a URN whose entity type matches the aspect (e.g. a
40+ * dataset URN for a dataset aspect, a dataJob URN for a dataJob aspect)
41+ * <li><b>Required:</b> {@link #provideSourceAspect()} — a realistic aspect payload at {@code
42+ * sourceVersion}, ready to be transformed
43+ * <li><b>Required:</b> {@link #assertTransformed(RecordTemplate)} — assertions that verify the
44+ * migration logic produced the correct result
45+ * <li><b>Optional:</b> {@link #configureRetrieverMock(AspectRetriever)} — stub retriever behavior
46+ * when the mutator looks up related aspects during the transform; {@link #mockRetriever} is
47+ * also accessible as a field for per-test stubs or {@code verify()} calls
4148 * </ol>
4249 *
4350 * <p>Example subclass:
4956 * return new MyAspectV1ToV2Migrator();
5057 * }
5158 *
59+ * @Override protected Urn entityUrn() {
60+ * return UrnUtils.getUrn("urn:li:dataset:(urn:li:dataPlatform:hive,test,PROD)");
61+ * }
62+ *
5263 * @Override protected RecordTemplate provideSourceAspect() {
5364 * Owner v1 = new Owner();
5465 * v1.setOldField("someValue");
6576 */
6677public abstract class AspectMigrationMutatorBaseTest {
6778
68- private static final Urn DATASET_URN =
69- UrnUtils .getUrn ("urn:li:dataset:(urn:li:dataPlatform:hive,test,PROD)" );
70-
7179 // Abstract
7280
7381 /** Returns the {@link AspectMigrationMutator} implementation under test. */
@@ -90,18 +98,36 @@ public abstract class AspectMigrationMutatorBaseTest {
9098 */
9199 protected abstract void assertTransformed (@ Nonnull RecordTemplate result );
92100
101+ /**
102+ * Returns the entity URN used when constructing test items. Must match the entity type that the
103+ * mutator's aspect belongs to (e.g. return a {@code dataJob} URN for a dataJob aspect mutator).
104+ */
105+ @ Nonnull
106+ protected abstract Urn entityUrn ();
107+
108+ /**
109+ * Hook for subclasses to configure additional retriever mock behavior. Called at the end of
110+ * {@link #setUp()} after the base stubs are in place. Override when the mutator under test uses
111+ * the retriever to compute the transform (e.g. to look up related aspects).
112+ */
113+ protected void configureRetrieverMock (@ Nonnull AspectRetriever retrieverMock ) {
114+ // no-op by default
115+ }
116+
93117 // Shared
94118
95119 protected EntityRegistry entityRegistry ;
96120 protected RetrieverContext retrieverContext ;
121+ protected AspectRetriever mockRetriever ;
97122
98123 @ BeforeMethod
99124 public void setUp () {
100125 entityRegistry = new TestEntityRegistry ();
101- AspectRetriever mockRetriever = mock (AspectRetriever .class );
126+ mockRetriever = mock (AspectRetriever .class );
102127 when (mockRetriever .getEntityRegistry ()).thenReturn (entityRegistry );
103128 retrieverContext = mock (RetrieverContext .class );
104129 when (retrieverContext .getAspectRetriever ()).thenReturn (mockRetriever );
130+ configureRetrieverMock (mockRetriever );
105131 }
106132
107133 // Version contract (auto-inherited)
@@ -146,7 +172,7 @@ public void writeMutation_atSourceVersion_mutatesAndBumpsVersion() {
146172 public void writeMutation_atDefaultSchemaVersion_mutatesAndBumpsVersion () {
147173 // null schemaVersion is treated as DEFAULT_SCHEMA_VERSION — only relevant when sourceVersion==1
148174 if (mutator ().getSourceVersion () != AspectMigrationMutator .DEFAULT_SCHEMA_VERSION ) {
149- return ;
175+ throw new SkipException ( "only applies when sourceVersion == DEFAULT_SCHEMA_VERSION" ) ;
150176 }
151177 ChangeMCP item = buildItem (provideSourceAspect (), new SystemMetadata ()); // no schemaVersion set
152178
@@ -155,36 +181,25 @@ public void writeMutation_atDefaultSchemaVersion_mutatesAndBumpsVersion() {
155181
156182 assertTrue (results .get (0 ).getSecond (), "Expected mutated=true for null schemaVersion (=1)" );
157183 assertEquals ((long ) item .getSystemMetadata ().getSchemaVersion (), mutator ().getTargetVersion ());
158- }
159-
160- @ Test
161- public void writeMutation_alreadyAtTargetVersion_isNoOp () {
162- SystemMetadata sm = new SystemMetadata ();
163- sm .setSchemaVersion (mutator ().getTargetVersion ());
164-
165- ChangeMCP item = buildItem (provideSourceAspect (), sm );
166-
167- List <Pair <ChangeMCP , Boolean >> results =
168- mutator ().writeMutation (List .of (item ), retrieverContext ).collect (Collectors .toList ());
169-
170- assertFalse (results .get (0 ).getSecond (), "Expected no-op when already at targetVersion" );
171- assertEquals (
172- (long ) item .getSystemMetadata ().getSchemaVersion (),
173- mutator ().getTargetVersion (),
174- "schemaVersion must remain unchanged" );
184+ assertTransformed (item .getRecordTemplate ());
175185 }
176186
177187 @ Test
178188 public void writeMutation_futureVersion_isNoOp () {
179189 SystemMetadata sm = new SystemMetadata ();
180- sm .setSchemaVersion (mutator ().getTargetVersion () + 1 ); // ahead of this mutator
190+ long futureVersion = mutator ().getTargetVersion () + 1 ;
191+ sm .setSchemaVersion (futureVersion ); // ahead of this mutator
181192
182193 ChangeMCP item = buildItem (provideSourceAspect (), sm );
183194
184195 List <Pair <ChangeMCP , Boolean >> results =
185196 mutator ().writeMutation (List .of (item ), retrieverContext ).collect (Collectors .toList ());
186197
187198 assertFalse (results .get (0 ).getSecond (), "Expected no-op for version ahead of targetVersion" );
199+ assertEquals (
200+ (long ) item .getSystemMetadata ().getSchemaVersion (),
201+ futureVersion ,
202+ "schemaVersion must not be modified for a future version" );
188203 }
189204
190205 // Read path (auto-inherited)
@@ -207,25 +222,13 @@ public void readMutation_atSourceVersion_mutatesAndBumpsVersion() {
207222 assertTransformed (item .getRecordTemplate ());
208223 }
209224
210- @ Test
211- public void readMutation_alreadyAtTargetVersion_isNoOp () {
212- SystemMetadata sm = new SystemMetadata ();
213- sm .setSchemaVersion (mutator ().getTargetVersion ());
214-
215- ChangeMCP item = buildItem (provideSourceAspect (), sm );
216-
217- List <Pair <ReadItem , Boolean >> results =
218- mutator ().readMutation (List .of (item ), retrieverContext ).collect (Collectors .toList ());
219-
220- assertFalse (results .get (0 ).getSecond (), "Expected no-op on read path when already migrated" );
221- }
222-
223225 // Helper
224226 protected ChangeMCP buildItem (RecordTemplate record , SystemMetadata sm ) {
227+ Urn urn = entityUrn ();
225228 return TestMCP .builder ()
226- .urn (DATASET_URN )
229+ .urn (urn )
227230 .changeType (ChangeType .UPSERT )
228- .entitySpec (entityRegistry .getEntitySpec ("dataset" ))
231+ .entitySpec (entityRegistry .getEntitySpec (urn . getEntityType () ))
229232 .aspectSpec (entityRegistry .getAspectSpecs ().get (mutator ().getAspectName ()))
230233 .recordTemplate (record )
231234 .systemMetadata (sm )
0 commit comments