Skip to content

Commit

Permalink
unsafeInstanceOf Quality, closes #155
Browse files Browse the repository at this point in the history
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
hamcrest/JavaHamcrest#388
  • Loading branch information
dmfs committed Dec 28, 2023
1 parent 7853662 commit f6da1cc
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
public final class InstanceOf<T> extends QualityComposition<T>
{
/**
* 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}.
* <p>
* This provides a type-safe way to downcast and apply a {@link Quality} of a subtype.
*
Expand All @@ -43,6 +43,8 @@ public final class InstanceOf<T> extends QualityComposition<T>
* assertThat(someObject,
* is(instanceOf(Number.class, that(has(Number::intValue, equalTo(1))))));
* }</pre>
*
* @see UnsafeInstanceOf#UnsafeInstanceOf(Class, Quality) for testing generic classes.
*/
public <V extends T> InstanceOf(Class<? extends V> expectation, Quality<? super V> delegate)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T> extends QualityComposition<T>
{
/**
* 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}.
* <p>
* 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.
*
* <h4>Example</h4>
* <pre>{@code
* Map actual = ...;
*
* assertThat(actual, unsafeInstanceOf(Map.class, Core.<Map<String, Object>>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"))))));
* }</pre>
* <p>
* <p>
* Be aware that this also allows you to write nonsensical tests like this:
* <pre>{@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))));
* }
* }</pre>
* <p>
* In such case, when the Quality is not compatible with the actual type, the test will fail, reporting a
* {@link ClassCastException}.
*/
public <V extends T, Q extends V> UnsafeInstanceOf(Class<V> expectation, Quality<? super Q> delegate)
{
super((Quality<T>) new AllOfFailingFast<>(NEW_LINE, new InstanceOf<>(expectation), delegate));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -300,4 +304,36 @@ void testDelegatingFunction()
)
);
}
}


@Test
void testHamcrestIssue107()
{
Map<String, Number> m = new HashMap<String, Number>();
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.<Map<String, Object>>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")))
)
));
}
}
Original file line number Diff line number Diff line change
@@ -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 <class java.lang.String>"),
new Fails<>(new Object(), "(0) instance of <class java.lang.Object>"),
new HasDescription("(0) instance of <class java.lang.Number>\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<Object>(1, "abc", true), new Seq(1, "abc", true)),
new Fails<>(new Seq<Object>(1.1, "abc", true), "(1) that iterated [ 0: <1.1>\n ... ]"),
new Fails<>(2, "(0) instance of <class java.lang.Integer>"),
new Fails<>("string", "(0) instance of <class java.lang.String>"),
new Fails<>(new Object(), "(0) instance of <class java.lang.Object>"),
new HasDescription("(0) instance of <interface java.lang.Iterable>\n (1) that iterates [ 0: <1>,\n 1: \"abc\",\n 2: <true> ]")
));
}
}

0 comments on commit f6da1cc

Please sign in to comment.