diff --git a/pom.xml b/pom.xml index 19caedc..d1ec903 100644 --- a/pom.xml +++ b/pom.xml @@ -92,5 +92,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) + ) + ); + } +}