All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.mutabilitydetector.unittesting.MutabilityAssert Maven / Gradle / Ivy

There is a newer version: 0.10.6
Show newest version
/*
 *    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

*
    *
  1. Preamble * *
  2. *
  3. Your first test case.
  4. *
  5. A more specific assertion
  6. *
  7. Allowing a reason for mutability
  8. * * *
* * * * *

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