|
||||||||||
PREV CLASS NEXT CLASS | FRAMES NO FRAMES | |||||||||
SUMMARY: NESTED | FIELD | CONSTR | METHOD | DETAIL: FIELD | CONSTR | METHOD |
java.lang.Object org.mutabilitydetector.unittesting.MutabilityAssert
public final class MutabilityAssert
Mutability Detector allows you to write a unit test that checks your classes are immutable.
The help contents here are also available on the project's JavaDoc.
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
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.
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.
import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
import static org.mutabilitydetector.unittesting.MutabilityMatchers.areEffectivelyImmutable;
import static org.mutabilitydetector.unittesting.AllowedReason.allowingNonFinalFields;
@Test public void checkMyClassIsImmutable() {
assertInstancesOf(MyClassWhereTheFieldsAreNotFinal.class,
areEffectivelyImmutable(),
allowingNonFinalFields());
}
See also:
The second parameter to the method
assertInstancesOf(Class, Matcher)
is a
Matcher<AnalysisResult>
, where Matcher
is a
hamcrest matcher, and
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 Matcher.matches(Object)
should return true for a test pass, false for a test failure.
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
assertInstancesOf(Class, Matcher, Matcher)
is a
Matcher<
. 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.
MutableReasonDetail
>
A
and
B
are immutable. However, if you write the assertion
assertImmutable(A.class);
, it will fail, as it can be subclassed
(see MutabilityReason.CAN_BE_SUBCLASSED
). To specifically allow this,
use the allowed reason: AllowedReason.allowingForSubclassing()
assertInstancesOf(A.class, areImmutable(), allowingForSubclassing());
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
Collections.unmodifiableList(List)
. For this scenario, use
AllowedReason.provided(Class)
.
To make the above example pass, use an allowed reason like so:
assertInstancesOf(MyImmutable.class,
areImmutable(),
AllowedReason.provided(ShouldAlsoBeImmutable.class).isAlsoImmutable());
Non-final fields If you have fields
which are neither mutated nor reassigned, you can suppress warnings about
them not being declared as final. Since the non-final field warning relates
to visibility in the Java Memory Model, and there are other ways to guarantee
visibility (e.g. assigning before a volatile write) it may be desirable.
Consider the following class:
public final class NonFinalField {
private String myField;
public NonFinalField(String myField) {
this.myField = myField;
}
public String getMyField() {
return myField;
}
}
This can be made to pass by allowing non-final fields, like so:
assertInstancesOf(NonFinalField.class,
areImmutable(),
AllowedReason.allowingNonFinalFields());
Safely copying into collection
field
Fields of collection types are normally interfaces (e.g. List, Set,
Iterable), and assigning these types to a field will result in a warning.
Mutability Detector has support for recognising the pattern of copying and
wrapping in an unmodifiable collection, however, it is limited to types and
methods from the standard JDK. Consider the following class:
import java.util.List;
public final class HasCollectionField {
private final List<String> myStrings;
public HasCollectionField(List<String> strings) {
List<String> copy = copyIntoNewList(strings);
List<String> unmodifiable = wrapWithUnmodifiable(strings);
this.myStrings = unmodifiable;
}
}
In this case we safely copy the list (copyIntoNewList
) and the
copy is then wrapped in an unmodifiable list that will prevent mutation (
wrapWithUnmodifiable
). However, since Mutability Detector is
unaware of these two methods, it will conclude that a mutable
List
type has been assigned to the private field.
This can be made to pass with the following:
assertInstancesOf(HasCollectionField.class,
areImmutable(),
AllowedReason.assumingFields("myStrings").areSafelyCopiedUnmodifiableCollectionsWithImmutableElements());
This also assumes that the collection contains only immutable elements, and
will suppress warnings generated when, for example, the field is a
List
of mutable Date
s.
Mutable field never modified
While it is absolutely possible to build an immutable object with mutable
fields, Mutability Detector errs on the side of caution. Thus, your class
could have a field of a mutable type, which neither escapes, nor is mutated
by the owning class, but still fails a test for immutability.
Consider the following class:
import java.util.Date;
public final class HasDateField {
private final Date myDate;
public HasDateField(Date date) {
this.myDate = new Date(date.getTime());
}
public Date getDate() {
return new Date(myDate.getTime());
}
}
A test for this class fails because the field myDate
is a
mutable type. This can be made to pass with the following:
assertInstancesOf(HasDateField.class,
areImmutable(),
AllowedReason.assumingFields("myDate").areNotModifiedAndDoNotEscape());
Caching values internally
As with String
, it is possible to reassign fields or mutate internal
state and still be immutable. As long as callers cannot observe the change
the class can be deemed immutable.
Consider the following class:
public final class MutatesAsInternalCaching {
private final String myString;
private final String otherString;
private int lengthWhenConcatenated;
public MutatesAsInternalCaching(String myString, String otherString) {
this.myString = myString;
this.otherString = otherString;
}
public int getConcatenatedLength() {
if (lengthWhenConcatenated == 0) {
lengthWhenConcatenated = myString.concat(otherString).length();
}
return lengthWhenConcatenated;
}
}
Here, the field lengthWhenConcatenated
is computed lazily. While
there is a field reassignment, which is a mutation, callers will never
perceive the mutation, as the calculation is done on the first request. Even
in a multithreaded environment, this is safe, and will result in no
observable mutation. Since the result is computed from other immutable
values, if multiple threads hit the race condition of seeing an empty value
while another thread is computing the result, the field will always be set to
the same value. Multiple assignments will appear as exactly one assignment,
just as with a final field.
This is called a 'benign data race', and exists in String
, with its
Object.hashCode()
method.
WARNING: This technique should be used with care, as it is very easy to get
wrong.
To allow this in tests, use an assertion like the following:
assertInstancesOf(MutatesAsInternalCaching.class,
areImmutable(),
AllowedReason.assumingFields("lengthWhenConcatenated").areModifiedAsPartOfAnUnobservableCachingStrategy());
This will also allow the use of mutable types and collections, not just
reassignments of primitive fields. Thus populating an array or collection for
future caching should also be allowed with this matcher.
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
assertInstancesOf(Class, Matcher, Matcher)
is a
Hamcrest Matcher<MutableReasonDetail
>
. For a
mutable class to pass the test, each MutableReasonDetail
of the
AnalysisResult
(provided by Mutability Detector) must be matched by
at least one allowed reason.
Configuring MutabilityAssert to use Hardcoded
Results
As of version 0.9, Mutability Detector uses a predefined list of hardcoded
results, in order to improve the accuracy of the analysis. For example, prior
to 0.9, java.lang.String was considered to be mutable. The out of the box hardcoded results includes
a non-exhaustive list of immutable classes from the standard JDK.
See also:
If you have found that Mutability Detector is unable to correctly analyse one
of your classes, or a class in a library you use, you may wish to add your
class to the list of predefined results. Follow these steps to choose your
own predefined list.
Why Would You Want To Hardcode Results?
Imagine a couple of classes like this:
@Immutable
public final class ActuallyImmutable {
// is immutable, but like java.lang.String, is incorrectly
// called mutable.
}
@Immutable
public final class UsesActuallyImmutable {
public final ActuallyImmutable myImmutableField = ...;
}
// in a test case
MutabilityAssert.assertImmutable(UsesActuallyImmutable.class); // this test fails
Because there's an error in the analysis of ActuallyImmutable
,
this "taints" UsesActuallyImmutable
, which will also be
considered mutable. Because of the transitive nature of a false positive,
this can cause Mutability Detector to think that entire object graphs are
mutable when they're not. Hardcoding your own results is a way to overcome
incorrect analysis.
Using A Different Asserter
To be able to hardcode results, you need your own instance of
MutabilityAsserter
. Normally assertions are made using the class
MutabilityAssert
. To choose different options from this class, you
must create and make available your own asserter with its own configuration.
Do this by constructing an instance of MutabilityAssert, like so:
public class SomeClassAccessibleByMyTests {
public static final MutabilityAsserter MUTABILITY = MutabilityAsserter.configured(...);
}
This allows your test case to have an assertion like:
// in a test case
MUTABILITY.assertImmutable(MyClass.class);
MutabilityAssert.configured()
method are not shown. The
parameter, of type Configuration
, is what will contain your hardcoded
results. In the following example, To overcome this, instantiate
MutabilityAsserter like this:
// as a field
MutabilityAsserter MUTABILITY = MutabilityAsserter.configured(new ConfigurationBuilder() {
@Override public void configure() {
hardcodeAsDefinitelyImmutable(ActuallyImmutable.class);
}
});
// in a test case
MUTABILITY.assertImmutable(UsesActuallyImmutable.class); // this now passes
Now classes which transitively depend on ActuallyImmutable
being
correctly analysed will not result in false positive results.
MUTABILITY.assertImmutable(ActuallyImmutable.class);
The test case will fail. Even though it's hardcoded, if you test it directly,
the result will reflect the real analysis. This is a deliberate choice, to
alert you to the possibility that your choice to hardcode a result may no
longer be valid. In this case you will want to write a an assertion which
allows the specific reasons for failure. You can still use the same asserter
you previously created for this, e.g.:
MUTABILITY.assertInstancesOf(ActuallyImmutable.class,
areImmutable(),
// configure your "allowed reasons" here
);
MutabilityMatchers
,
AllowedReason
,
AnalysisResult
,
MutableReasonDetail
,
IsImmutable
,
Configuration
,
Configurations.OUT_OF_THE_BOX_CONFIGURATION
,
ConfigurationBuilder
,
MutabilityReason
Method Summary | |
---|---|
static void |
assertImmutable(Class<?> expectedImmutableClass)
Checks that the given class is immutable, or fails with an AssertionError . |
static void |
assertInstancesOf(Class<?> clazz,
org.hamcrest.Matcher<AnalysisResult> mutabilityMatcher)
Checks that the result of analysis satisfies the given Matcher ,
or fails with an AssertionError . |
static void |
assertInstancesOf(Class<?> clazz,
org.hamcrest.Matcher<AnalysisResult> mutabilityMatcher,
Iterable<org.hamcrest.Matcher<MutableReasonDetail>> allowingAll)
Checks that the result of analysis satisfies the given Matcher ,
while allowing mismatches in the form of allowed reasons, or fails with
an AssertionError . |
static void |
assertInstancesOf(Class<?> clazz,
org.hamcrest.Matcher<AnalysisResult> mutabilityMatcher,
org.hamcrest.Matcher<MutableReasonDetail> allowing)
Checks that the result of analysis satisfies the given Matcher ,
while allowing mismatches in the form of an allowed reason, or fails with
an AssertionError . |
static void |
assertInstancesOf(Class<?> clazz,
org.hamcrest.Matcher<AnalysisResult> mutabilityMatcher,
org.hamcrest.Matcher<MutableReasonDetail> allowingFirst,
org.hamcrest.Matcher<MutableReasonDetail> allowingSecond)
Checks that the result of analysis satisfies the given Matcher ,
while allowing mismatches in the form of allowed reasons, or fails with
an AssertionError . |
static void |
assertInstancesOf(Class<?> clazz,
org.hamcrest.Matcher<AnalysisResult> mutabilityMatcher,
org.hamcrest.Matcher<MutableReasonDetail> allowingFirst,
org.hamcrest.Matcher<MutableReasonDetail> allowingSecond,
org.hamcrest.Matcher<MutableReasonDetail> allowingThird)
Checks that the result of analysis satisfies the given Matcher ,
while allowing mismatches in the form of allowed reasons, or fails with
an AssertionError . |
static void |
assertInstancesOf(Class<?> clazz,
org.hamcrest.Matcher<AnalysisResult> mutabilityMatcher,
org.hamcrest.Matcher<MutableReasonDetail> allowingFirst,
org.hamcrest.Matcher<MutableReasonDetail> allowingSecond,
org.hamcrest.Matcher<MutableReasonDetail> allowingThird,
org.hamcrest.Matcher<MutableReasonDetail>... allowingRest)
Checks that the result of analysis satisfies the given Matcher ,
while allowing mismatches in the form of allowed reasons, or fails with
an AssertionError . |
Methods inherited from class java.lang.Object |
---|
clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Method Detail |
---|
public static void assertImmutable(Class<?> expectedImmutableClass)
AssertionError
.
Example:
MutabilityAssert.assertImmutable(HopefullyImmutable.class);
expectedImmutableClass
- IsImmutable.IMMUTABLE
public static void assertInstancesOf(Class<?> clazz, org.hamcrest.Matcher<AnalysisResult> mutabilityMatcher)
Matcher
,
or fails with an AssertionError
.
The given matcher will be invoked with the AnalysisResult
produced by Mutability Detector's analysis of the given class. The most
common matchers can be found at MutabilityMatchers
.
Example:
MutabilityAssert.assertImmutable(HopefullyImmutable.class,
MutabilityMatchers.areImmutable());
MutabilityAssert.assertImmutable(HopefullyEffectivelyImmutable.class,
MutabilityMatchers.areEffectivelyImmutable());
MutabilityMatchers.areImmutable()
,
MutabilityMatchers.areEffectivelyImmutable()
,
Matcher
,
AnalysisResult
,
IsImmutable.IMMUTABLE
,
IsImmutable.EFFECTIVELY_IMMUTABLE
public static void assertInstancesOf(Class<?> clazz, org.hamcrest.Matcher<AnalysisResult> mutabilityMatcher, org.hamcrest.Matcher<MutableReasonDetail> allowing)
Matcher
,
while allowing mismatches in the form of an allowed reason, or fails with
an AssertionError
.
The given matcher will be invoked with the AnalysisResult
produced by Mutability Detector's analysis of the given class. The most
common matchers can be found at MutabilityMatchers
.
The given allowed reason will be used to determine if any of the
MutableReasonDetail
attached to the AnalysisResult
have
been explicitly permitted by the unit test. If any of the reasons have
not been allowed, an AssertionError will be thrown.
Several out-of-the-box allowed reasons can be found at
AllowedReason
.
Example:
MutabilityAssert.assertImmutable(HopefullyImmutable.class,
MutabilityMatchers.areImmutable(),
AllowedReason.allowingForSubclassing());
MutableReasonDetail
,
AllowedReason
,
AllowedReason.allowingForSubclassing()
,
MutabilityMatchers.areImmutable()
public static void assertInstancesOf(Class<?> clazz, org.hamcrest.Matcher<AnalysisResult> mutabilityMatcher, org.hamcrest.Matcher<MutableReasonDetail> allowingFirst, org.hamcrest.Matcher<MutableReasonDetail> allowingSecond)
Matcher
,
while allowing mismatches in the form of allowed reasons, or fails with
an AssertionError
.
The given matcher will be invoked with the AnalysisResult
produced by Mutability Detector's analysis of the given class. The most
common matchers can be found at MutabilityMatchers
.
The given allowed reason will be used to determine if any of the
MutableReasonDetail
attached to the AnalysisResult
have
been explicitly permitted by the unit test. If any of the reasons have
not been allowed, an AssertionError will be thrown.
Several out-of-the-box allowed reasons can be found at
AllowedReason
.
Example:
MutabilityAssert.assertImmutable(HopefullyImmutable.class,
MutabilityMatchers.areImmutable(),
AllowedReason.allowingForSubclassing(),
AllowedReason.allowingNonFinalFields());
MutableReasonDetail
,
AllowedReason
,
AllowedReason.allowingForSubclassing()
,
MutabilityMatchers.areImmutable()
public static void assertInstancesOf(Class<?> clazz, org.hamcrest.Matcher<AnalysisResult> mutabilityMatcher, org.hamcrest.Matcher<MutableReasonDetail> allowingFirst, org.hamcrest.Matcher<MutableReasonDetail> allowingSecond, org.hamcrest.Matcher<MutableReasonDetail> allowingThird)
Matcher
,
while allowing mismatches in the form of allowed reasons, or fails with
an AssertionError
.
Alternative version of
assertInstancesOf(Class, Matcher, Matcher)
which takes more
allowed reasons.
MutableReasonDetail
,
AllowedReason
,
AllowedReason.allowingForSubclassing()
,
AllowedReason.allowingNonFinalFields()
,
MutabilityMatchers.areImmutable()
public static void assertInstancesOf(Class<?> clazz, org.hamcrest.Matcher<AnalysisResult> mutabilityMatcher, org.hamcrest.Matcher<MutableReasonDetail> allowingFirst, org.hamcrest.Matcher<MutableReasonDetail> allowingSecond, org.hamcrest.Matcher<MutableReasonDetail> allowingThird, org.hamcrest.Matcher<MutableReasonDetail>... allowingRest)
Matcher
,
while allowing mismatches in the form of allowed reasons, or fails with
an AssertionError
.
Alternative version of
assertInstancesOf(Class, Matcher, Matcher)
which takes more
allowed reasons.
MutableReasonDetail
,
AllowedReason
,
AllowedReason.allowingForSubclassing()
,
AllowedReason.allowingNonFinalFields()
,
MutabilityMatchers.areImmutable()
public static void assertInstancesOf(Class<?> clazz, org.hamcrest.Matcher<AnalysisResult> mutabilityMatcher, Iterable<org.hamcrest.Matcher<MutableReasonDetail>> allowingAll)
Matcher
,
while allowing mismatches in the form of allowed reasons, or fails with
an AssertionError
.
Alternative version of
assertInstancesOf(Class, Matcher, Matcher)
which takes an
iterable of allowed reasons.
MutableReasonDetail
,
AllowedReason
,
AllowedReason.allowingForSubclassing()
,
AllowedReason.allowingNonFinalFields()
,
MutabilityMatchers.areImmutable()
|
||||||||||
PREV CLASS NEXT CLASS | FRAMES NO FRAMES | |||||||||
SUMMARY: NESTED | FIELD | CONSTR | METHOD | DETAIL: FIELD | CONSTR | METHOD |