org.mutabilitydetector.unittesting
Class MutabilityAssert

java.lang.Object
  extended by org.mutabilitydetector.unittesting.MutabilityAssert

public final class MutabilityAssert
extends Object

Mutability Detector

Mutability Detector allows you to write a unit test that checks your classes are immutable.

Help Guide

Contents

  1. Preamble
  2. Your first test case.
  3. A more specific assertion
  4. Allowing a reason for mutability
  5. Hardcoding analysis results

About this help guide

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 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;
 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.

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 assertInstancesOf(Class, Matcher, Matcher) is a Matcher<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 MutabilityReason.CAN_BE_SUBCLASSED). To specifically allow this, use the allowed reason: 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 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 Dates.

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);
 

Hardcoding Analysis Results

Notice in the above example, the parameters given to the 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.

Testing Hardcoded Classes Directly

Using the configuration from above, if we have the assertion:
 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 
                              );
 

Author:
Graham Allan / Grundlefleck at gmail dot com
See Also:
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

assertImmutable

public static void assertImmutable(Class<?> expectedImmutableClass)
Checks that the given class is immutable, or fails with an AssertionError.

Example:


 MutabilityAssert.assertImmutable(HopefullyImmutable.class);
 

Parameters:
expectedImmutableClass -
See Also:
IsImmutable.IMMUTABLE

assertInstancesOf

public 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.

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());
 
 

See Also:
MutabilityMatchers.areImmutable(), MutabilityMatchers.areEffectivelyImmutable(), Matcher, AnalysisResult, IsImmutable.IMMUTABLE, IsImmutable.EFFECTIVELY_IMMUTABLE

assertInstancesOf

public 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.

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());
 
 
 
 

See Also:
MutableReasonDetail, AllowedReason, AllowedReason.allowingForSubclassing(), MutabilityMatchers.areImmutable()

assertInstancesOf

public 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.

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());
 
 

See Also:
MutableReasonDetail, AllowedReason, AllowedReason.allowingForSubclassing(), MutabilityMatchers.areImmutable()

assertInstancesOf

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)
Checks that the result of analysis satisfies the given 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.

See Also:
MutableReasonDetail, AllowedReason, AllowedReason.allowingForSubclassing(), AllowedReason.allowingNonFinalFields(), MutabilityMatchers.areImmutable()

assertInstancesOf

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)
Checks that the result of analysis satisfies the given 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.

See Also:
MutableReasonDetail, AllowedReason, AllowedReason.allowingForSubclassing(), AllowedReason.allowingNonFinalFields(), MutabilityMatchers.areImmutable()

assertInstancesOf

public 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.

Alternative version of assertInstancesOf(Class, Matcher, Matcher) which takes an iterable of allowed reasons.

See Also:
MutableReasonDetail, AllowedReason, AllowedReason.allowingForSubclassing(), AllowedReason.allowingNonFinalFields(), MutabilityMatchers.areImmutable()


Copyright © 2013. All Rights Reserved.