diff --git a/pom.xml b/pom.xml
index 2ef5780..92d09ad 100644
--- a/pom.xml
+++ b/pom.xml
@@ -58,5 +58,12 @@
5.10.0
test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ 5.10.0
+ test
+
diff --git a/src/test/java/ovh/gasser/newshapes/shapes/ShapeContractTest.java b/src/test/java/ovh/gasser/newshapes/shapes/ShapeContractTest.java
new file mode 100644
index 0000000..0e54932
--- /dev/null
+++ b/src/test/java/ovh/gasser/newshapes/shapes/ShapeContractTest.java
@@ -0,0 +1,73 @@
+package ovh.gasser.newshapes.shapes;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.awt.Rectangle;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Parameterized contract tests verifying Shape interface invariants
+ * across all implementations (SRectangle, SCircle, STriangle, SText, SCollection).
+ */
+class ShapeContractTest {
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("ovh.gasser.newshapes.shapes.ShapeFactory#allShapes")
+ void cloneReturnsIndependentCopy(Shape shape) {
+ Shape cloned = shape.clone();
+
+ // clone must not be the same instance
+ assertNotSame(shape, cloned, "clone() must return a new instance");
+
+ // clone must have equal bounds
+ assertEquals(shape.getBounds(), cloned.getBounds(),
+ "clone() must preserve bounds");
+
+ // mutating the clone must not affect the original
+ Rectangle originalBounds = shape.getBounds();
+ cloned.translate(999, 999);
+ assertEquals(originalBounds, shape.getBounds(),
+ "Translating the clone must not affect the original's bounds");
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("ovh.gasser.newshapes.shapes.ShapeFactory#allShapes")
+ void getBoundsReturnsCopy(Shape shape) {
+ Rectangle bounds1 = shape.getBounds();
+ Rectangle bounds2 = shape.getBounds();
+
+ // successive calls must return equal bounds
+ assertEquals(bounds1, bounds2,
+ "getBounds() must return consistent values");
+
+ // but not the same object (defensive copy)
+ assertNotSame(bounds1, bounds2,
+ "getBounds() must return a copy, not internal state");
+
+ // mutating the returned Rectangle must not affect the shape
+ bounds1.translate(500, 500);
+ assertEquals(bounds2, shape.getBounds(),
+ "Mutating the returned Rectangle must not affect the shape");
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("ovh.gasser.newshapes.shapes.ShapeFactory#allShapes")
+ void translateMutatesInPlace(Shape shape) {
+ Rectangle before = shape.getBounds();
+ int dx = 7, dy = -3;
+
+ shape.translate(dx, dy);
+
+ Rectangle after = shape.getBounds();
+ assertEquals(before.x + dx, after.x,
+ "translate() must shift x by dx");
+ assertEquals(before.y + dy, after.y,
+ "translate() must shift y by dy");
+ assertEquals(before.width, after.width,
+ "translate() must not change width");
+ assertEquals(before.height, after.height,
+ "translate() must not change height");
+ }
+}
diff --git a/src/test/java/ovh/gasser/newshapes/shapes/ShapeFactory.java b/src/test/java/ovh/gasser/newshapes/shapes/ShapeFactory.java
new file mode 100644
index 0000000..6d15db0
--- /dev/null
+++ b/src/test/java/ovh/gasser/newshapes/shapes/ShapeFactory.java
@@ -0,0 +1,25 @@
+package ovh.gasser.newshapes.shapes;
+
+import java.awt.Color;
+import java.util.stream.Stream;
+
+/**
+ * Provides Shape instances for parameterized contract tests.
+ */
+public final class ShapeFactory {
+
+ private ShapeFactory() {}
+
+ static Stream allShapes() {
+ return Stream.of(
+ SRectangle.create(10, 20, 100, 50),
+ SCircle.create(5, 5, 30),
+ STriangle.create(0, 0, 40, Color.RED, Color.BLACK),
+ SText.create(15, 25, "Hello"),
+ SCollection.of(
+ SRectangle.create(0, 0, 20, 20),
+ SCircle.create(10, 10, 5)
+ )
+ );
+ }
+}