org.mutabilitydetector.unittesting.MutabilityAssert Maven / Gradle / Ivy
Show all versions of MutabilityDetector Show documentation
/*
* Copyright (c) 2008-2011 Graham Allan
*
* 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.mutabilitydetector.unittesting;
import static java.util.Arrays.asList;
import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
import static org.mutabilitydetector.unittesting.matchers.reasons.WithAllowedReasonsMatcher.withAllowedReasons;
import static org.mutabilitydetector.unittesting.matchers.reasons.WithAllowedReasonsMatcher.withNoAllowedReasons;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.hamcrest.Matcher;
import org.mutabilitydetector.AnalysisResult;
import org.mutabilitydetector.IsImmutable;
import org.mutabilitydetector.MutabilityReason;
import org.mutabilitydetector.MutableReasonDetail;
import org.mutabilitydetector.unittesting.internal.AnalysisSessionHolder;
import org.mutabilitydetector.unittesting.internal.AssertionReporter;
import org.mutabilitydetector.unittesting.matchers.reasons.WithAllowedReasonsMatcher;
/**
*
* Mutability Detector
*
* Mutability Detector allows you to write a unit test that checks your classes are immutable.
*
* Help Guide
* Contents
*
* - Preamble
*
*
* - Your first test case.
* - A more specific assertion
* - Allowing a reason for mutability
*
*
*
*
*
*
*
* About this help guide
*
* The help contents here are also available on the project homepage, currently at http://code.google.com/p/mutability-detector/.
*
* This style of documentation is used as it provides content suitable for a web page and for offline use in the JavaDoc
* viewer of your favourite IDE. It has been shamelessly stolen from inspired by the Mockito project,
* thanks guys.
*
* About these examples
I am assuming JUnit as the unit testing library. However, Mutability
* Detector should work with any unit testing library that uses the exception mechanism for their assertions, such as
* TestNG. If Mutability Detector is incompatible with your favourite testing library, please get in touch, and we'll
* see what we can do about that.
*
* Your first test case.
*
* The most simple assertion you can make will look something like this:
*
*
*
* import static org.mutabilitydetector.unittesting.MutabilityAssert.assertImmutable;
*
* @Test public void checkMyClassIsImmutable() {
* assertImmutable(MyClass.class);
* }
*
*
*
*
* This assertion will trigger an analysis of MyClass
, passing if found to be immutable, failing if found
* to be mutable.
*
*
*
* Configuring the assertion
*
* The method used above is a shortcut for more expressive forms of the assertion, and does not allow any further
* configuration. An equivalent assertion is:
*
*
*
*
* import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
* import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;
*
* @Test public void checkMyClassIsImmutable() {
* assertInstancesOf(MyClass.class, areImmutable());
* }
*
*
*
* This is the form that can be used for extra configuration of the assertion. Let's take a look at an assertion that is
* configured differently. Consider a class which is immutable, except for fields not being declared final
.
* According to Java Concurrency In Practice, instances of classes like this, as long as
* they are safely publised are still considered effectively immutable. Please note however, Mutability
* Detector does not check that objects are safely published.
* To represent this in a unit test, the assertion would like this:
*
*
*
* import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
* import static org.mutabilitydetector.unittesting.MutabilityMatchers.areEffectivelyImmutable;
*
* @Test public void checkMyClassIsImmutable() {
* assertInstancesOf(MyClassWhereTheFieldsAreNotFinal.class, areEffectivelyImmutable());
* }
*
*
* See also:
*
* - {@link IsImmutable#EFFECTIVELY_IMMUTABLE}
*
*
*
* The second parameter to the method {@link MutabilityAssert#assertInstancesOf(Class, Matcher)} is a
* Matcher<AnalysisResult>
, where Matcher
is a hamcrest matcher, and {@link AnalysisResult} is provided by Mutability
* Detector to represent the result of the static analysis performed on the given class. This means, if none of the
* out-of-the-box matchers are quite right for your scenario, you can supply your own. Your implementation of
* {@link Matcher#matches(Object)} should return true for a test pass, false for a test failure.
*
*
*
* Allowing a reason
* There can also be cases where your class is found to be mutable, but you know for your scenario that it's an
* acceptable reason. Consider the following class:
*
*
* public abstract class AbstractIntHolder {
* private final int intField;
*
* public AbstractIntHolder(int intToStore) {
* this.intField = intToStore;
* }
* }
*
*
*
* In this case, if you assert AbstractIntHolder
is immutable, the test will fail. This is because
* AbstractIntHolder can be subclassed, which means clients of this class, who for example, accept parameters of this
* type and store them to fields, cannot depend on receiving a concrete, immutable object. If, in your code, you know
* that all subclasses will also be immutable (hopefully you have tests for them too) then you can say that it is okay
* that this class can be subclassed, because you know all subclasses are immutable as well.
*
*
* Given such a scenario, the way to get your test to pass, and still provide a check that the class doesn't become
* mutable by some other cause, is to allow a reason for mutability. An example of allowing said reason for
* AbstractIntHolder
could look like this:
*
*
*
*
* import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
* import static org.mutabilitydetector.unittesting.MutabilityMatchers.areEffectivelyImmutable;
* import static org.mutabilitydetector.unittesting.AllowedReason.allowingForSubclassing;
*
* @Test public void checkMyClassIsImmutable() {
* assertInstancesOf(AbstractIntHolder.class, areImmutable(), allowingForSubclassing());
* }
*
*
*
* This will allow your test to pass, but fail for any other reasons that are introduced, e.g. if someone adds a setter
* method.
*
*
* Similar to the Matcher<AnalysisResult>
parameter, the allowed reason parameter of
* {@link #assertInstancesOf(Class, Matcher, Matcher)} is a Matcher<{@link MutableReasonDetail}>
.
* Mutability Detector will provide only a few out-of-the-box implementations for this, which are unlikely to cover each
* scenario where you want to permit a certain aspect of mutability.
*
* Out-of-the-box allowed reasons
*
*
Abstract class with immutable implementation
* It can be useful to write an abstract class, designed for extension, which is immutable. To ensure that a concrete
* class B, extending abstract class A is immutable, it is necessary to test that both A
and B
* are immutable. However, if you write the assertion assertImmutable(A.class);
, it will fail, as it can be
* subclassed (see {@link MutabilityReason#CAN_BE_SUBCLASSED}). To specifically allow this, use the allowed reason:
* {@link AllowedReason#allowingForSubclassing()}
*
* For example:
* assertInstancesOf(A.class, areImmutable(), allowingForSubclassing());
*
* Depending on other classes being immutable
* Consider the following code:
*
* public final class MyImmutable {
* public final ShouldAlsoBeImmutable field;
*
* public MyImmutable(ShouldAlsoBeImmutable dependsOnThisBeingImmutable) {
* this.field = dependsOnThisBeingImmutable;
* }
* }
*
*
*
* If ShouldAlsoBeImmutable
is not a concrete class (an interface
or abstract
* class), assertImmutable(MyImmutable.class);
will fail, as there's no guarantee that the runtime
* implementation of ShouldBeImmutable
is actually immutable. A common example is taking a parameter of
* java.util.List
, where you require that it is an immutable implementation, e.g: a copy created with
* {@link Collections#unmodifiableList(List)}. For this scenario, use {@link AllowedReason#provided(Class)}.
*
* To make the above example pass, use an allowed reason like so:
*
*
*
*
*
* assertInstancesOf(MyImmutable.class, areImmutable(),
* AllowedReason.provided(ShouldAlsoBeImmutable.class).isAlsoImmutable());
*
*
*
* Using {@link String} fields in immutable objects
* Until The String Problem is resolved,
* using provided(String.class).isAlsoImmutable()
is the only practical solution for when your immutable
* classes have {@link String} fields. This sucks, I know :-(
*
*
*
* Writing your own allowed reasons
*
* If none of the out-of-the-box allowed reasons suit your needs, it is possible to supply your own implementation. The
* allowed reason in the signature of {@link MutabilityAssert#assertInstancesOf(Class, Matcher, Matcher)} is a
* Hamcrest Matcher<{@link MutableReasonDetail}>
. For a mutable class to pass the test, each
* {@link MutableReasonDetail} of the {@link AnalysisResult} (provided by Mutability Detector) must be matched by at least one allowed reason.
*
*
* @author Graham Allan / Grundlefleck at gmail dot com
*
* @see MutabilityMatchers
* @see AllowedReason
* @see AnalysisResult
* @see MutableReasonDetail
* @see IsImmutable
*
*/
public final class MutabilityAssert {
private MutabilityAssert() { }
private final static AssertionReporter reporter = new AssertionReporter();
public static void assertImmutable(Class> expectedImmutableClass) {
reporter.assertThat(getResultFor(expectedImmutableClass), withNoAllowedReasons(areImmutable()));
}
public static void assertInstancesOf(Class> clazz, Matcher mutabilityMatcher) {
reporter.assertThat(getResultFor(clazz), withNoAllowedReasons(mutabilityMatcher));
}
@SuppressWarnings("unchecked")
public static void assertInstancesOf(Class> clazz,
Matcher mutabilityMatcher,
Matcher allowing) {
WithAllowedReasonsMatcher areImmutable_withReasons = withAllowedReasons(mutabilityMatcher, asList((allowing)));
reporter.assertThat(getResultFor(clazz), areImmutable_withReasons);
}
@SuppressWarnings("unchecked")
public static void assertInstancesOf(Class> clazz,
Matcher mutabilityMatcher,
Matcher allowingFirst,
Matcher allowingSecond) {
WithAllowedReasonsMatcher areImmutable_withReasons = withAllowedReasons(mutabilityMatcher,
asList(allowingFirst, allowingSecond));
reporter.assertThat(getResultFor(clazz), areImmutable_withReasons);
}
@SuppressWarnings("unchecked")
public static void assertInstancesOf(Class> clazz,
Matcher mutabilityMatcher,
Matcher allowingFirst,
Matcher allowingSecond,
Matcher allowingThird) {
WithAllowedReasonsMatcher areImmutable_withReasons = withAllowedReasons(mutabilityMatcher,
asList(allowingFirst,
allowingSecond,
allowingThird));
reporter.assertThat(getResultFor(clazz), areImmutable_withReasons);
}
public static void assertInstancesOf(Class> clazz,
Matcher mutabilityMatcher,
Matcher allowingFirst,
Matcher allowingSecond,
Matcher allowingThird,
Matcher... allowingRest) {
List> allowedReasonMatchers = new ArrayList>();
allowedReasonMatchers.add(allowingFirst);
allowedReasonMatchers.add(allowingSecond);
allowedReasonMatchers.add(allowingThird);
allowedReasonMatchers.addAll(asList(allowingRest));
WithAllowedReasonsMatcher areImmutable_withReasons = withAllowedReasons(mutabilityMatcher, allowedReasonMatchers);
reporter.assertThat(getResultFor(clazz), areImmutable_withReasons);
}
private static AnalysisResult getResultFor(Class> clazz) {
return AnalysisSessionHolder.analysisResultFor(clazz);
}
}