From f6da1cc9a0e9ff46c2f30718f827b40889a67caf Mon Sep 17 00:00:00 2001 From: Marten Gajda Date: Thu, 28 Dec 2023 17:55:17 +0100 Subject: [PATCH] unsafeInstanceOf Quality, closes #155 This provides a Quality similar to `istanceOf` but also taking `Quality`s of subclasses of the given class. This is unsafe because there is no guarantee that the testee is actually of that subtype, but it may be required if the testee is a generic class. It solves issues like described in https://github.com/hamcrest/JavaHamcrest/issues/388 --- .../confidence/quality/object/InstanceOf.java | 6 +- .../quality/object/UnsafeInstanceOf.java | 75 +++++++++++++++++++ .../org/saynotobugs/confidence/Examples.java | 38 +++++++++- .../quality/object/UnsafeInstanceOfTest.java | 63 ++++++++++++++++ 4 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/UnsafeInstanceOf.java create mode 100644 confidence-core/src/test/java/org/saynotobugs/confidence/quality/object/UnsafeInstanceOfTest.java diff --git a/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/InstanceOf.java b/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/InstanceOf.java index 78b6d76..3059ba2 100644 --- a/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/InstanceOf.java +++ b/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/InstanceOf.java @@ -33,8 +33,8 @@ public final class InstanceOf extends QualityComposition { /** - * A {@link Quality} that matches when the object under test is an instance of the given class and satisfies the - * given {@link Quality}. + * A {@link Quality} that is satisfied when the object under test is an instance of the given class and satisfies + * the given {@link Quality}. *

* This provides a type-safe way to downcast and apply a {@link Quality} of a subtype. * @@ -43,6 +43,8 @@ public final class InstanceOf extends QualityComposition * assertThat(someObject, * is(instanceOf(Number.class, that(has(Number::intValue, equalTo(1)))))); * } + * + * @see UnsafeInstanceOf#UnsafeInstanceOf(Class, Quality) for testing generic classes. */ public InstanceOf(Class expectation, Quality delegate) { diff --git a/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/UnsafeInstanceOf.java b/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/UnsafeInstanceOf.java new file mode 100644 index 0000000..74c57dc --- /dev/null +++ b/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/UnsafeInstanceOf.java @@ -0,0 +1,75 @@ +/* + * Copyright 2022 dmfs GmbH + * + * + * 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. + * + */ + +package org.saynotobugs.confidence.quality.object; + +import org.dmfs.srcless.annotations.staticfactory.StaticFactories; +import org.saynotobugs.confidence.Quality; +import org.saynotobugs.confidence.quality.composite.AllOfFailingFast; +import org.saynotobugs.confidence.quality.composite.QualityComposition; + +import static org.saynotobugs.confidence.description.LiteralDescription.NEW_LINE; + + +@StaticFactories(value = "Core", packageName = "org.saynotobugs.confidence.quality") +public final class UnsafeInstanceOf extends QualityComposition +{ + /** + * A {@link Quality} that matches when the object under test is an instance of any subclass of the given class and + * satisfies the given {@link Quality}. + *

+ * This works like {@link InstanceOf} but provides fewer type-safety guarantees allowing you to pass + * {@link Quality} of subtypes of {@code V}. This may be required when testing generic classes, because you'll + * essentially be forced to work with raw types in such case. + * + *

Example

+ *
{@code
+     * Map actual = ...;
+     *
+     * assertThat(actual, unsafeInstanceOf(Map.class, Core.>allOf(
+     *     containsEntry("k1", unsafeInstanceOf(Map.class, allOf(
+     *         containsEntry("k11", "v11"),
+     *         containsEntry("k12", "v12")))),
+     *     containsEntry("k2", unsafeInstanceOf(Iterable.class, iterates("v21", "v22"))),
+     *     containsEntry("k3", unsafeInstanceOf(String.class, equalTo("v3"))))));
+     * }
+ *

+ *

+ * Be aware that this also allows you to write nonsensical tests like this: + *

{@code
+     *     static class C1 {}
+     *
+     *     static class C2 extends C1 {
+     *         int bar() {return 2;}
+     *     }
+     *
+     *     @Test
+     *     void instanceOfTest() {
+     *         Object o = new C1();
+     *         assertThat(o, unsafeInstanceOf(C1.class, has(C2::bar, equalTo(2))));
+     *     }
+     * }
+ *

+ * In such case, when the Quality is not compatible with the actual type, the test will fail, reporting a + * {@link ClassCastException}. + */ + public UnsafeInstanceOf(Class expectation, Quality delegate) + { + super((Quality) new AllOfFailingFast<>(NEW_LINE, new InstanceOf<>(expectation), delegate)); + } +} diff --git a/confidence-core/src/test/java/org/saynotobugs/confidence/Examples.java b/confidence-core/src/test/java/org/saynotobugs/confidence/Examples.java index 4484c95..f33feae 100644 --- a/confidence-core/src/test/java/org/saynotobugs/confidence/Examples.java +++ b/confidence-core/src/test/java/org/saynotobugs/confidence/Examples.java @@ -8,6 +8,7 @@ import org.saynotobugs.confidence.description.NumberDescription; import org.saynotobugs.confidence.description.Spaced; import org.saynotobugs.confidence.description.Text; +import org.saynotobugs.confidence.quality.Core; import org.saynotobugs.confidence.quality.comparable.GreaterThan; import org.saynotobugs.confidence.quality.comparable.LessThan; import org.saynotobugs.confidence.quality.compat.Hamcrest; @@ -27,6 +28,9 @@ import org.saynotobugs.confidence.test.quality.Fails; import org.saynotobugs.confidence.test.quality.Passes; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; @@ -300,4 +304,36 @@ void testDelegatingFunction() ) ); } -} + + + @Test + void testHamcrestIssue107() + { + Map m = new HashMap(); + Integer foo = new Integer(6); + m.put("foo", foo); + assertThat(m, containsEntry("foo", foo)); + } + + + @Test + void testHamcrestIssue388() + { + Map actual = Map.of( + "k1", Map.of( + "k11", "v11", + "k12", "v12"), + "k2", List.of("v21", "v22"), + "k3", "v3" + ); + + assertThat(actual, unsafeInstanceOf(Map.class, Core.>allOf( + containsEntry("k1", unsafeInstanceOf(Map.class, allOf( + containsEntry("k11", "v11"), + containsEntry("k12", "v12")))), + containsEntry("k2", unsafeInstanceOf(Iterable.class, iterates("v21", "v22"))), + containsEntry("k3", unsafeInstanceOf(String.class, equalTo("v3"))) + ) + )); + } +} \ No newline at end of file diff --git a/confidence-core/src/test/java/org/saynotobugs/confidence/quality/object/UnsafeInstanceOfTest.java b/confidence-core/src/test/java/org/saynotobugs/confidence/quality/object/UnsafeInstanceOfTest.java new file mode 100644 index 0000000..48f7c82 --- /dev/null +++ b/confidence-core/src/test/java/org/saynotobugs/confidence/quality/object/UnsafeInstanceOfTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023 dmfs GmbH + * + * + * 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. + * + */ + +package org.saynotobugs.confidence.quality.object; + +import org.dmfs.jems2.iterable.Seq; +import org.junit.jupiter.api.Test; +import org.saynotobugs.confidence.quality.composite.AllOf; +import org.saynotobugs.confidence.quality.composite.Has; +import org.saynotobugs.confidence.quality.grammar.That; +import org.saynotobugs.confidence.quality.iterable.Iterates; +import org.saynotobugs.confidence.test.quality.Fails; +import org.saynotobugs.confidence.test.quality.HasDescription; +import org.saynotobugs.confidence.test.quality.Passes; + +import static org.saynotobugs.confidence.Assertion.assertThat; + +class UnsafeInstanceOfTest +{ + + @Test + void testDelegate() + { + assertThat(new UnsafeInstanceOf<>(Number.class, new That<>(new Has<>("intValue", Number::intValue, new EqualTo<>(1)))), + new AllOf<>( + new Passes<>(1, 1.001, 1L, 1f), + new Fails<>(0.999, "(1) that had intValue <0>"), + new Fails<>(2, "(1) that had intValue <2>"), + new Fails<>("string", "(0) instance of "), + new Fails<>(new Object(), "(0) instance of "), + new HasDescription("(0) instance of \n (1) that has intValue <1>") + )); + } + + @Test + void testSubClassDelegate() + { + assertThat(new UnsafeInstanceOf<>(Iterable.class, new That<>(new Iterates<>(1, "abc", true))), + new AllOf<>( + new Passes<>(new Seq(1, "abc", true), new Seq(1, "abc", true)), + new Fails<>(new Seq(1.1, "abc", true), "(1) that iterated [ 0: <1.1>\n ... ]"), + new Fails<>(2, "(0) instance of "), + new Fails<>("string", "(0) instance of "), + new Fails<>(new Object(), "(0) instance of "), + new HasDescription("(0) instance of \n (1) that iterates [ 0: <1>,\n 1: \"abc\",\n 2: ]") + )); + } +} \ No newline at end of file