diff --git lucene/src/test/org/apache/lucene/search/similarities/AfterEffect.java lucene/src/test/org/apache/lucene/search/similarities/AfterEffect.java
index 8c2662f..faee912 100644
--- lucene/src/test/org/apache/lucene/search/similarities/AfterEffect.java
+++ lucene/src/test/org/apache/lucene/search/similarities/AfterEffect.java
@@ -47,5 +47,17 @@ public abstract class AfterEffect {
     public final Explanation explain(EasyStats stats, float tfn) {
       return new Explanation(1, "no aftereffect");
     }
+    
+    @Override
+    public String toString() {
+      return "";
+    }
   }
+  
+  /**
+   * Subclasses must override this method to return the code of the
+   * after effect formula. Refer to the original paper for the list. 
+   */
+  @Override
+  public abstract String toString();
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/AfterEffectB.java lucene/src/test/org/apache/lucene/search/similarities/AfterEffectB.java
index 0edc2e2..90079b2 100644
--- lucene/src/test/org/apache/lucene/search/similarities/AfterEffectB.java
+++ lucene/src/test/org/apache/lucene/search/similarities/AfterEffectB.java
@@ -41,4 +41,9 @@ public class AfterEffectB extends AfterEffect {
     result.addDetail(new Explanation(stats.getDocFreq(), "docFreq"));
     return result;
   }
+
+  @Override
+  public String toString() {
+    return "B";
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/AfterEffectL.java lucene/src/test/org/apache/lucene/search/similarities/AfterEffectL.java
index b20d784..3a1f223 100644
--- lucene/src/test/org/apache/lucene/search/similarities/AfterEffectL.java
+++ lucene/src/test/org/apache/lucene/search/similarities/AfterEffectL.java
@@ -37,4 +37,9 @@ public class AfterEffectL extends AfterEffect {
     result.addDetail(new Explanation(tfn, "tfn"));
     return result;
   }
+  
+  @Override
+  public String toString() {
+    return "L";
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/BasicModel.java lucene/src/test/org/apache/lucene/search/similarities/BasicModel.java
index d07a6fe..e811869 100644
--- lucene/src/test/org/apache/lucene/search/similarities/BasicModel.java
+++ lucene/src/test/org/apache/lucene/search/similarities/BasicModel.java
@@ -50,4 +50,11 @@ public abstract class BasicModel {
         new Explanation(stats.getTotalTermFreq(), "totalTermFreq"));
     return result;
   }
+  
+  /**
+   * Subclasses must override this method to return the code of the
+   * basic model formula. Refer to the original paper for the list. 
+   */
+  @Override
+  public abstract String toString();
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/BasicModelBE.java lucene/src/test/org/apache/lucene/search/similarities/BasicModelBE.java
index 366fce3..ea55a46 100644
--- lucene/src/test/org/apache/lucene/search/similarities/BasicModelBE.java
+++ lucene/src/test/org/apache/lucene/search/similarities/BasicModelBE.java
@@ -36,4 +36,9 @@ public class BasicModelBE extends BasicModel {
   private final double f(long n, float m) {
     return (m + 0.5) * log2((double)n / m) + (n - m) * log2(n);
   }
+  
+  @Override
+  public String toString() {
+    return "Be";
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/BasicModelD.java lucene/src/test/org/apache/lucene/search/similarities/BasicModelD.java
index b477bc9..cb8dc7d 100644
--- lucene/src/test/org/apache/lucene/search/similarities/BasicModelD.java
+++ lucene/src/test/org/apache/lucene/search/similarities/BasicModelD.java
@@ -34,4 +34,9 @@ public class BasicModelD extends BasicModel {
     double D = phi * log2(phi / p) + nphi * log2(nphi / (1 - p));
     return (float)(D * F + 0.5 * log2(2 * Math.PI * tfn * nphi));
   }
+  
+  @Override
+  public String toString() {
+    return "D";
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/BasicModelG.java lucene/src/test/org/apache/lucene/search/similarities/BasicModelG.java
index 864156e..a8f7d2f 100644
--- lucene/src/test/org/apache/lucene/search/similarities/BasicModelG.java
+++ lucene/src/test/org/apache/lucene/search/similarities/BasicModelG.java
@@ -26,8 +26,13 @@ import static org.apache.lucene.search.similarities.EasySimilarity.log2;
 public class BasicModelG extends BasicModel {
   @Override
   public final float score(EasyStats stats, float tfn) {
-    double lambda = stats.getTotalTermFreq() / stats.getNumberOfDocuments();
+    double lambda = (double)stats.getTotalTermFreq() / stats.getNumberOfDocuments();
     // -log(1 / (lambda + 1)) -> log(lambda + 1)
     return (float)(log2(lambda + 1) + tfn * log2((1 + lambda) / lambda));
   }
+
+  @Override
+  public String toString() {
+    return "G";
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/BasicModelIF.java lucene/src/test/org/apache/lucene/search/similarities/BasicModelIF.java
index f096b0b..5cc1db5 100644
--- lucene/src/test/org/apache/lucene/search/similarities/BasicModelIF.java
+++ lucene/src/test/org/apache/lucene/search/similarities/BasicModelIF.java
@@ -31,4 +31,9 @@ public class BasicModelIF extends BasicModel {
     long F = stats.getTotalTermFreq();
     return tfn * (float)(log2((N + 1) / (F + 0.5)));
   }
+
+  @Override
+  public String toString() {
+    return "I(F)";
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/BasicModelIn.java lucene/src/test/org/apache/lucene/search/similarities/BasicModelIn.java
index 4556850..3d7bf96 100644
--- lucene/src/test/org/apache/lucene/search/similarities/BasicModelIn.java
+++ lucene/src/test/org/apache/lucene/search/similarities/BasicModelIn.java
@@ -44,4 +44,9 @@ public class BasicModelIn extends BasicModel {
         new Explanation(stats.getDocFreq(), "docFreq"));
     return result;
   }
+
+  @Override
+  public String toString() {
+    return "I(n)";
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/BasicModelIne.java lucene/src/test/org/apache/lucene/search/similarities/BasicModelIne.java
index 042ff99..7989cb9 100644
--- lucene/src/test/org/apache/lucene/search/similarities/BasicModelIne.java
+++ lucene/src/test/org/apache/lucene/search/similarities/BasicModelIne.java
@@ -32,4 +32,9 @@ public class BasicModelIne extends BasicModel {
     double ne = N * (1 - Math.pow((N - 1) / N, F));
     return tfn * (float)(log2((N + 1) / (ne + 0.5)));
   }
+
+  @Override
+  public String toString() {
+    return "I(ne)";
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/BasicModelP.java lucene/src/test/org/apache/lucene/search/similarities/BasicModelP.java
index 308f5d6..654133a 100644
--- lucene/src/test/org/apache/lucene/search/similarities/BasicModelP.java
+++ lucene/src/test/org/apache/lucene/search/similarities/BasicModelP.java
@@ -31,4 +31,9 @@ public class BasicModelP extends BasicModel {
         + (lambda + 1 / 12 / tfn - tfn) * log2(Math.E)
         + 0.5 * log2(2 * Math.PI * tfn));
   }
+
+  @Override
+  public String toString() {
+    return "P";
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/DFRSimilarity.java lucene/src/test/org/apache/lucene/search/similarities/DFRSimilarity.java
index 16119de..ec6c420 100644
--- lucene/src/test/org/apache/lucene/search/similarities/DFRSimilarity.java
+++ lucene/src/test/org/apache/lucene/search/similarities/DFRSimilarity.java
@@ -97,4 +97,10 @@ public class DFRSimilarity extends EasySimilarity {
     expl.addDetail(basicModel.explain(stats, tfn));
     expl.addDetail(afterEffect.explain(stats, tfn));
   }
+
+  @Override
+  public String toString() {
+    return "DFR " + basicModel.toString() + afterEffect.toString()
+                  + normalization.toString();
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/Distribution.java lucene/src/test/org/apache/lucene/search/similarities/Distribution.java
index 11f2bcb..aa2702a 100644
--- lucene/src/test/org/apache/lucene/search/similarities/Distribution.java
+++ lucene/src/test/org/apache/lucene/search/similarities/Distribution.java
@@ -35,4 +35,11 @@ public abstract class Distribution {
     return new Explanation(
         score(stats, tfn, lambda), getClass().getSimpleName());
   }
+  
+  /**
+   * Subclasses must override this method to return the name of the
+   * distribution. 
+   */
+  @Override
+  public abstract String toString();
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/DistributionLL.java lucene/src/test/org/apache/lucene/search/similarities/DistributionLL.java
index de5375b..f7a4a4a 100644
--- lucene/src/test/org/apache/lucene/search/similarities/DistributionLL.java
+++ lucene/src/test/org/apache/lucene/search/similarities/DistributionLL.java
@@ -29,4 +29,9 @@ public class DistributionLL extends Distribution {
   public final float score(EasyStats stats, float tfn, float lambda) {
     return (float)-Math.log(lambda / (tfn + lambda));
   }
+  
+  @Override
+  public String toString() {
+    return "LL";
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/DistributionSPL.java lucene/src/test/org/apache/lucene/search/similarities/DistributionSPL.java
index 892d7dc..fa9ed02 100644
--- lucene/src/test/org/apache/lucene/search/similarities/DistributionSPL.java
+++ lucene/src/test/org/apache/lucene/search/similarities/DistributionSPL.java
@@ -31,4 +31,9 @@ public class DistributionSPL extends Distribution {
     return (float)-Math.log(
         (Math.pow(lambda, (tfn / (tfn + 1))) - lambda) / (1 - lambda));
   }
+  
+  @Override
+  public String toString() {
+    return "SPL";
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/EasySimilarity.java lucene/src/test/org/apache/lucene/search/similarities/EasySimilarity.java
index 204bbdc..e95ea50 100644
--- lucene/src/test/org/apache/lucene/search/similarities/EasySimilarity.java
+++ lucene/src/test/org/apache/lucene/search/similarities/EasySimilarity.java
@@ -166,6 +166,13 @@ public abstract class EasySimilarity extends Similarity {
     return new EasySloppyDocScorer((EasyStats) stats,
                                    context.reader.norms(fieldName));
   }
+  
+  /**
+   * Subclasses must override this method to return the name of the Similarity
+   * and preferably the values of parameters (if any) as well.
+   */
+  @Override
+  public abstract String toString();  // nocommit: to Similarity?
 
   // ------------------------------ Norm handling ------------------------------
   
diff --git lucene/src/test/org/apache/lucene/search/similarities/IBSimilarity.java lucene/src/test/org/apache/lucene/search/similarities/IBSimilarity.java
index 15a967e..15c9de7 100644
--- lucene/src/test/org/apache/lucene/search/similarities/IBSimilarity.java
+++ lucene/src/test/org/apache/lucene/search/similarities/IBSimilarity.java
@@ -84,4 +84,16 @@ public class IBSimilarity extends EasySimilarity {
     expl.addDetail(distribution.explain(
         stats, normExpl.getValue(), lambdaExpl.getValue()));
   }
+  
+  /**
+   * The name of IB methods follow the pattern
+   * {@code IB <distribution> <lambda><normalization>}. The name of the
+   * distribution is the same as in the original paper; for the names of lambda
+   * parameters, refer to the javadoc of the {@link Lambda} classes.
+   */
+  @Override
+  public String toString() {
+    return "IB " + distribution.toString() + "-" + lambda.toString()
+                 + normalization.toString();
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/LMDirichletSimilarity.java lucene/src/test/org/apache/lucene/search/similarities/LMDirichletSimilarity.java
index 60f7a68..005e9fb 100644
--- lucene/src/test/org/apache/lucene/search/similarities/LMDirichletSimilarity.java
+++ lucene/src/test/org/apache/lucene/search/similarities/LMDirichletSimilarity.java
@@ -83,4 +83,9 @@ public class LMDirichletSimilarity extends LMSimilarity {
   public float getMu() {
     return mu;
   }
+  
+  @Override
+  public String getName() {
+    return String.format("Dirichlet(%f)", getMu());
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/LMJelinekMercerSimilarity.java lucene/src/test/org/apache/lucene/search/similarities/LMJelinekMercerSimilarity.java
index 2b1375f..a0b3860 100644
--- lucene/src/test/org/apache/lucene/search/similarities/LMJelinekMercerSimilarity.java
+++ lucene/src/test/org/apache/lucene/search/similarities/LMJelinekMercerSimilarity.java
@@ -69,4 +69,9 @@ public class LMJelinekMercerSimilarity extends LMSimilarity {
   public float getLambda() {
     return lambda;
   }
+
+  @Override
+  public String getName() {
+    return String.format("Jelinek-Mercer(%f)", getLambda());
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/LMSimilarity.java lucene/src/test/org/apache/lucene/search/similarities/LMSimilarity.java
index 0abea21..971a4d1 100644
--- lucene/src/test/org/apache/lucene/search/similarities/LMSimilarity.java
+++ lucene/src/test/org/apache/lucene/search/similarities/LMSimilarity.java
@@ -71,6 +71,30 @@ public abstract class LMSimilarity extends EasySimilarity {
     expl.addDetail(new Explanation(collectionModel.computeProbability(stats),
                                    "collection probability"));
   }
+  
+  /**
+   * Returns the name of the LM method. The values of the parameters should be
+   * included as well.
+   * <p>Used in {@link #toString()}</p>.
+   */
+  public abstract String getName();
+  
+  /**
+   * Returns the name of the LM method. If a custom collection model strategy is
+   * used, its name is included as well.
+   * @see #getName()
+   * @see CollectionModel#getName()
+   * @see DefaultCollectionModel 
+   */
+  @Override
+  public String toString() {
+    String coll = collectionModel.getName();
+    if (coll != null) {
+      return String.format("LM %s - %s", getName(), coll);
+    } else {
+      return String.format("LM %s", getName());
+    }
+  }
 
   /** Stores the collection distribution of the current term. */
   public static class LMStats extends EasyStats {
@@ -105,6 +129,9 @@ public abstract class LMSimilarity extends EasySimilarity {
      * strategy for the current term.
      */
     public float computeProbability(EasyStats stats);
+    
+    /** The name of the collection model strategy. */
+    public String getName();
   }
   
   /**
@@ -116,5 +143,10 @@ public abstract class LMSimilarity extends EasySimilarity {
     public float computeProbability(EasyStats stats) {
       return (float)stats.getTotalTermFreq() / stats.getNumberOfFieldTokens();
     }
+    
+    @Override
+    public String getName() {
+      return null;
+    }
   }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/Lambda.java lucene/src/test/org/apache/lucene/search/similarities/Lambda.java
index 4b658f7..5df1b4a 100644
--- lucene/src/test/org/apache/lucene/search/similarities/Lambda.java
+++ lucene/src/test/org/apache/lucene/search/similarities/Lambda.java
@@ -30,4 +30,13 @@ public abstract class Lambda {
   public abstract float lambda(EasyStats stats);
   /** Explains the lambda parameter. */
   public abstract Explanation explain(EasyStats stats);
+  
+  /**
+   * Subclasses must override this method to return the code of the lambda
+   * formula. Since the original paper is not very clear on this matter, and
+   * also uses the DFR naming scheme incorrectly, the codes here were chosen
+   * arbitrarily.
+   */
+  @Override
+  public abstract String toString();
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/LambdaDF.java lucene/src/test/org/apache/lucene/search/similarities/LambdaDF.java
index f8d7b30..5001b0e 100644
--- lucene/src/test/org/apache/lucene/search/similarities/LambdaDF.java
+++ lucene/src/test/org/apache/lucene/search/similarities/LambdaDF.java
@@ -40,4 +40,9 @@ public class LambdaDF extends Lambda {
         new Explanation(stats.getNumberOfDocuments(), "numberOfDocuments"));
     return result;
   }
+  
+  @Override
+  public String toString() {
+    return "D";
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/LambdaTTF.java lucene/src/test/org/apache/lucene/search/similarities/LambdaTTF.java
index 0fd3b99..06cbb05 100644
--- lucene/src/test/org/apache/lucene/search/similarities/LambdaTTF.java
+++ lucene/src/test/org/apache/lucene/search/similarities/LambdaTTF.java
@@ -40,4 +40,9 @@ public class LambdaTTF extends Lambda {
         new Explanation(stats.getNumberOfDocuments(), "numberOfDocuments"));
     return result;
   }
+  
+  @Override
+  public String toString() {
+    return "L";
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/Normalization.java lucene/src/test/org/apache/lucene/search/similarities/Normalization.java
index a9a9d4f..68ae759 100644
--- lucene/src/test/org/apache/lucene/search/similarities/Normalization.java
+++ lucene/src/test/org/apache/lucene/search/similarities/Normalization.java
@@ -59,5 +59,17 @@ public abstract class Normalization {
     public final Explanation explain(EasyStats stats, float tf, int len) {
       return new Explanation(1, "no normalization");
     }
+    
+    @Override
+    public String toString() {
+      return "";
+    }
   }
+  
+  /**
+   * Subclasses must override this method to return the code of the
+   * normalization formula. Refer to the original paper for the list. 
+   */
+  @Override
+  public abstract String toString();
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/NormalizationH1.java lucene/src/test/org/apache/lucene/search/similarities/NormalizationH1.java
index eab4ef6..4201c59 100644
--- lucene/src/test/org/apache/lucene/search/similarities/NormalizationH1.java
+++ lucene/src/test/org/apache/lucene/search/similarities/NormalizationH1.java
@@ -25,4 +25,9 @@ public class NormalizationH1 extends Normalization {
   public final float tfn(EasyStats stats, float tf, int len) {
     return tf * stats.getAvgFieldLength() / len;
   }
+
+  @Override
+  public String toString() {
+    return "1";
+  }
 }
diff --git lucene/src/test/org/apache/lucene/search/similarities/NormalizationH2.java lucene/src/test/org/apache/lucene/search/similarities/NormalizationH2.java
index df3d4f4..5d24eca 100644
--- lucene/src/test/org/apache/lucene/search/similarities/NormalizationH2.java
+++ lucene/src/test/org/apache/lucene/search/similarities/NormalizationH2.java
@@ -28,4 +28,9 @@ public class NormalizationH2 extends Normalization {
   public final float tfn(EasyStats stats, float tf, int len) {
     return (float)(tf * log2(1 + stats.getAvgFieldLength() / len));
   }
+
+  @Override
+  public String toString() {
+    return "2";
+  }
 }
\ No newline at end of file
diff --git lucene/src/test/org/apache/lucene/search/similarities/TestEasySimilarity.java lucene/src/test/org/apache/lucene/search/similarities/TestEasySimilarity.java
new file mode 100644
index 0000000..e1ebc9b
--- /dev/null
+++ lucene/src/test/org/apache/lucene/search/similarities/TestEasySimilarity.java
@@ -0,0 +1,436 @@
+package org.apache.lucene.search.similarities;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.SimilarityProvider;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.LuceneTestCase;
+
+/**
+ * Tests the {@link EasySimilarity}-based Similarities. Contains unit tests and
+ * integration tests as well. This class maintains a list of
+ * {@code EasySimilarity} subclasses. Each test case performs its test on all
+ * items in the list. If a test case fails, the name of the Similarity that
+ * caused the failure is returned as part of the assertion error message.
+ * <p>Unit testing is performed by constructing statistics manually and calling
+ * the {@link EasySimilarity#score(EasyStats, float, int)} method of the
+ * Similarities. The statistics represent corner cases of corpus distributions.
+ * </p>
+ * <p>For the integration tests, a small (8-document) collection is indexed. The
+ * tests verify that for a specific query, all relevant documents are returned
+ * in the correct order. The collection consists of two poems of English poet
+ * <a href="http://en.wikipedia.org/wiki/William_blake">William Blake</a>.</p>
+ * <p>Note: the list of Similarities is maintained by hand. If a new Similarity
+ * is added to the {@code org.apache.lucene.search.similarities} package, the
+ * list should be updated accordingly.</p>
+ */
+public class TestEasySimilarity extends LuceneTestCase {
+  private static String FIELD_BODY = "body";
+  private static String FIELD_ID = "id";
+  /** The DFR basic models to test. */
+  private static BasicModel[] BASIC_MODELS;
+  /** The DFR aftereffects to test. */
+  private static AfterEffect[] AFTER_EFFECTS;
+  /** The DFR normalizations to test. */
+  private static Normalization[] NORMALIZATIONS;
+  /** The distributions for IB. */
+  private static Distribution[] DISTRIBUTIONS;
+  /** Lambdas for IB. */
+  private static Lambda[] LAMBDAS;
+  
+  static {
+    BASIC_MODELS = new BasicModel[] {
+        new BasicModelBE(), new BasicModelD(), new BasicModelG(),
+        new BasicModelIF(), new BasicModelIn(), new BasicModelIne(),
+        new BasicModelP()
+    };
+    AFTER_EFFECTS = new AfterEffect[] {
+        new AfterEffectB(), new AfterEffectL(), new AfterEffect.NoAfterEffect()
+    };
+    NORMALIZATIONS = new Normalization[] {
+        new NormalizationH1(), new NormalizationH2(),
+        new Normalization.NoNormalization()
+    };
+    DISTRIBUTIONS = new Distribution[] {
+        new DistributionLL(), new DistributionSPL()
+    };
+    LAMBDAS = new Lambda[] {
+        new LambdaDF(), new LambdaTTF()
+    };
+  }
+  
+  private IndexSearcher searcher;
+  private Directory dir;
+  private IndexReader reader;
+  /** The list of similarities to test. */
+  private List<EasySimilarity> sims;
+  
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+
+    dir = newDirectory();
+    RandomIndexWriter writer = new RandomIndexWriter(random, dir);
+
+    for (int i = 0; i < docs.length; i++) {
+      Document d = new Document();
+      d.add(newField(FIELD_ID, Integer.toString(i), Field.Store.YES, Field.Index.NO));
+      d.add(newField(FIELD_BODY, docs[i], Field.Index.ANALYZED));
+      writer.addDocument(d);
+    }
+    
+    reader = writer.getReader();
+    searcher = newSearcher(reader);
+    writer.close();
+    
+    sims = new ArrayList<EasySimilarity>();
+    for (BasicModel basicModel : BASIC_MODELS) {
+      for (AfterEffect afterEffect : AFTER_EFFECTS) {
+        for (Normalization normalization : NORMALIZATIONS) {
+          sims.add(new DFRSimilarity(basicModel, afterEffect, normalization));
+        }
+      }
+    }
+    for (Distribution distribution : DISTRIBUTIONS) {
+      for (Lambda lambda : LAMBDAS) {
+        for (Normalization normalization : NORMALIZATIONS) {
+          sims.add(new IBSimilarity(distribution, lambda, normalization));
+        }
+      }
+    }
+    sims.add(new LMDirichletSimilarity());
+    sims.add(new LMJelinekMercerSimilarity(0.1f));
+    sims.add(new LMJelinekMercerSimilarity(0.7f));
+  }
+  
+  // ------------------------------- Unit tests --------------------------------
+  
+  /** The default number of documents in the unit tests. */
+  private static int NUMBER_OF_DOCUMENTS = 100;
+  /** The default total number of tokens in the field in the unit tests. */
+  private static long NUMBER_OF_FIELD_TOKENS = 5000;
+  /** The default average field length in the unit tests. */
+  private static float AVG_FIELD_LENGTH = 50;
+  /** The default document frequency in the unit tests. */
+  private static int DOC_FREQ = 10;
+  /**
+   * The default total number of occurrences of this term across all documents
+   * in the unit tests.
+   */
+  private static long TOTAL_TERM_FREQ = 70;
+  
+  /** The default tf in the unit tests. */
+  private static float FREQ = 7;
+  /** The default document length in the unit tests. */
+  private static int DOC_LEN = 40;
+  
+  /** Creates the default statistics object that the specific tests modify. */
+  private EasyStats createStats() {
+    EasyStats stats = new EasyStats(1);
+    stats.setNumberOfDocuments(NUMBER_OF_DOCUMENTS);
+    stats.setNumberOfFieldTokens(NUMBER_OF_FIELD_TOKENS);
+    stats.setAvgFieldLength(AVG_FIELD_LENGTH);
+    stats.setDocFreq(DOC_FREQ);
+    stats.setTotalTermFreq(TOTAL_TERM_FREQ);
+    return stats;
+  }
+
+  /**
+   * The generic test core called by all unit test methods. It calls the
+   * {@link EasySimilarity#score(EasyStats, float, int)} method of all
+   * Similarities in {@link #sims} and checks if the score is valid; i.e. it
+   * is a finite positive real number.
+   */
+  private void unitTestCore(EasyStats stats, float freq, int docLen)
+      throws IOException {
+    for (EasySimilarity sim : sims) {
+      float score = sim.score(stats, freq, docLen);
+      assertFalse("Score infinite: " + sim.toString(), Float.isInfinite(score));
+      assertFalse("Score NaN: " + sim.toString(), Float.isNaN(score));
+      assertTrue("Score negative: " + sim.toString(), score >= 0);
+    }
+  }
+  
+  /** Runs the unit test with the default statistics. */
+  public void testDefault() throws IOException {
+    unitTestCore(createStats(), FREQ, DOC_LEN);
+  }
+  
+  /**
+   * Tests correct behavior when
+   * {@code numberOfDocuments = numberOfFieldTokens}.
+   */
+  public void testSparseDocuments() throws IOException {
+    EasyStats stats = createStats();
+    stats.setNumberOfFieldTokens(stats.getNumberOfDocuments());
+    stats.setTotalTermFreq(stats.getDocFreq());
+    stats.setAvgFieldLength(
+        stats.getNumberOfFieldTokens() / stats.getNumberOfDocuments());
+    unitTestCore(stats, FREQ, DOC_LEN);
+  }
+
+  /**
+   * Tests correct behavior when
+   * {@code numberOfDocuments > numberOfFieldTokens}.
+   */
+  public void testVerySparseDocuments() throws IOException {
+    EasyStats stats = createStats();
+    stats.setNumberOfFieldTokens(stats.getNumberOfDocuments() * 2 / 3);
+    stats.setTotalTermFreq(stats.getDocFreq());
+    stats.setAvgFieldLength(
+        stats.getNumberOfFieldTokens() / stats.getNumberOfDocuments());
+    unitTestCore(stats, FREQ, DOC_LEN);
+  }
+
+  /**
+   * Tests correct behavior when
+   * {@code NumberOfDocuments = 1}.
+   */
+  public void testOneDocument() throws IOException {
+    EasyStats stats = createStats();
+    stats.setNumberOfDocuments(1);
+    stats.setNumberOfFieldTokens(DOC_LEN);
+    stats.setAvgFieldLength(DOC_LEN);
+    stats.setDocFreq(1);
+    stats.setTotalTermFreq((int)FREQ);
+    unitTestCore(stats, FREQ, DOC_LEN);
+  }
+
+  /**
+   * Tests correct behavior when
+   * {@code docFreq = numberOfDocuments}.
+   */
+  public void testAllDocumentsRelevant() throws IOException {
+    EasyStats stats = createStats();
+    float mult = (0.0f + stats.getNumberOfDocuments()) / stats.getDocFreq();
+    stats.setTotalTermFreq((int)(stats.getTotalTermFreq() * mult));
+    stats.setDocFreq(stats.getNumberOfDocuments());
+    unitTestCore(stats, FREQ, DOC_LEN);
+  }
+
+  /**
+   * Tests correct behavior when
+   * {@code docFreq > numberOfDocuments / 2}.
+   */
+  public void testMostDocumentsRelevant() throws IOException {
+    EasyStats stats = createStats();
+    float mult = (0.6f * stats.getNumberOfDocuments()) / stats.getDocFreq();
+    stats.setTotalTermFreq((int)(stats.getTotalTermFreq() * mult));
+    stats.setDocFreq((int)(stats.getNumberOfDocuments() * 0.6));
+    unitTestCore(stats, FREQ, DOC_LEN);
+  }
+
+  /**
+   * Tests correct behavior when
+   * {@code docFreq = 1}.
+   */
+  public void testOnlyOneRelevantDocument() throws IOException {
+    EasyStats stats = createStats();
+    stats.setDocFreq(1);
+    stats.setTotalTermFreq((int)FREQ + 3);
+    unitTestCore(stats, FREQ, DOC_LEN);
+  }
+
+  /**
+   * Tests correct behavior when
+   * {@code totalTermFreq = numberOfFieldTokens}.
+   */
+  public void testAllTermsRelevant() throws IOException {
+    EasyStats stats = createStats();
+    stats.setTotalTermFreq(stats.getNumberOfFieldTokens());
+    unitTestCore(stats, DOC_LEN, DOC_LEN);
+    // nocommit docLen > avglength
+  }
+
+  /**
+   * Tests correct behavior when
+   * {@code totalTermFreq > numberOfDocuments}.
+   */
+  public void testMoreTermsThanDocuments() throws IOException {
+    EasyStats stats = createStats();
+    stats.setTotalTermFreq(
+        stats.getTotalTermFreq() + stats.getNumberOfDocuments());
+    unitTestCore(stats, 2 * FREQ, DOC_LEN);
+  }
+
+  /**
+   * Tests correct behavior when
+   * {@code totalTermFreq = numberOfDocuments}.
+   */
+  public void testNumberOfTermsAsDocuments() throws IOException {
+    EasyStats stats = createStats();
+    stats.setTotalTermFreq(stats.getNumberOfDocuments());
+    unitTestCore(stats, FREQ, DOC_LEN);
+  }
+
+  /**
+   * Tests correct behavior when {@code totalTermFreq = 1}.
+   */
+  public void testOneTerm() throws IOException {
+    EasyStats stats = createStats();
+    stats.setDocFreq(1);
+    stats.setTotalTermFreq(1);
+    unitTestCore(stats, 1, DOC_LEN);
+  }
+  
+  /**
+   * Tests correct behavior when {@code totalTermFreq = freq}.
+   */
+  public void testOneRelevantDocument() throws IOException {
+    EasyStats stats = createStats();
+    stats.setDocFreq(1);
+    stats.setTotalTermFreq((int)FREQ);
+    unitTestCore(stats, FREQ, DOC_LEN);
+  }
+  
+  /**
+   * Tests correct behavior when {@code numberOfFieldTokens = freq}.
+   */
+  public void testAllTermsRelevantOnlyOneDocument() throws IOException {
+    EasyStats stats = createStats();
+    stats.setNumberOfDocuments(10);
+    stats.setNumberOfFieldTokens(50);
+    stats.setAvgFieldLength(5);
+    stats.setDocFreq(1);
+    stats.setTotalTermFreq(50);
+    unitTestCore(stats, 50, 50);
+  }
+
+  /**
+   * Tests correct behavior when there is only one document with a single term 
+   * in the collection.
+   */
+  public void testOnlyOneTermOneDocument() throws IOException {
+    EasyStats stats = createStats();
+    stats.setNumberOfDocuments(1);
+    stats.setNumberOfFieldTokens(1);
+    stats.setAvgFieldLength(1);
+    stats.setDocFreq(1);
+    stats.setTotalTermFreq(1);
+    unitTestCore(stats, 1, 1);
+  }
+
+  /**
+   * Tests correct behavior when there is only one term in the field, but
+   * more than one documents.
+   */
+  public void testOnlyOneTerm() throws IOException {
+    EasyStats stats = createStats();
+    stats.setNumberOfFieldTokens(1);
+    stats.setAvgFieldLength(1.0f / stats.getNumberOfDocuments());
+    stats.setDocFreq(1);
+    stats.setTotalTermFreq(1);
+    unitTestCore(stats, 1, DOC_LEN);
+  }
+  
+  /**
+   * Tests correct behavior when {@code avgFieldLength = docLen}.
+   */
+  public void testDocumentLengthAverage() throws IOException {
+    EasyStats stats = createStats();
+    unitTestCore(stats, FREQ, (int)stats.getAvgFieldLength());
+  }
+  
+  // ---------------------------- Integration tests ----------------------------
+
+  /** The "collection" for the integration tests. */
+  String[] docs = new String[] {
+      "Tiger, tiger burning bright   In the forest of the night   What immortal hand or eye   Could frame thy fearful symmetry ?",
+      "In what distant depths or skies   Burnt the fire of thine eyes ?   On what wings dare he aspire ?   What the hands the seize the fire ?",
+      "And what shoulder and what art   Could twist the sinews of thy heart ?   And when thy heart began to beat What dread hand ? And what dread feet ?",
+      "What the hammer? What the chain ?   In what furnace was thy brain ?   What the anvil ? And what dread grasp   Dare its deadly terrors clasp ?",
+      "And when the stars threw down their spears   And water'd heaven with their tear   Did he smile his work to see ?   Did he, who made the lamb, made thee ?",
+      "Tiger, tiger burning bright   In the forest of the night   What immortal hand or eye   Dare frame thy fearful symmetry ?",
+      "Cruelty has a human heart   And jealousy a human face   Terror the human form divine   And Secrecy the human dress .",
+      "The human dress is forg'd iron   The human form a fiery forge   The human face a furnace seal'd   The human heart its fiery gorge ."
+  };
+  
+  /**
+   * Tests whether all similarities return three documents for the query word
+   * "heart".
+   */
+  public void testHeartList() throws IOException {
+    Query q = new TermQuery(new Term(FIELD_BODY, "heart"));
+    
+    for (EasySimilarity sim : sims) {
+      searcher.setSimilarityProvider(new EasySimilarityProvider(sim));
+      TopDocs topDocs = searcher.search(q, 1000);
+      assertEquals("Failed: " + sim.toString(), 3, topDocs.totalHits);
+    }
+  }
+  
+  /** Test whether all similarities return document 3 before documents 7 and 8. */
+  public void testHeartRanking() throws IOException {
+    Query q = new TermQuery(new Term(FIELD_BODY, "heart"));
+    
+    for (EasySimilarity sim : sims) {
+      searcher.setSimilarityProvider(new EasySimilarityProvider(sim));
+      TopDocs topDocs = searcher.search(q, 1000);
+      assertEquals("Failed: " + sim.toString(), 2, topDocs.scoreDocs[0].doc);
+    }
+  }
+  
+  @Override
+  public void tearDown() throws Exception {
+    searcher.close();
+    reader.close();
+    dir.close();
+    super.tearDown();
+  }
+  
+  /**
+   * A simple Similarity provider that returns in {@code get(String field)} the
+   * object passed to its constructor.
+   */
+  // nocommit a real EasySimilarityProvider with coord and queryNorm implemented?
+  public static class EasySimilarityProvider implements SimilarityProvider {
+    private EasySimilarity sim;
+    
+    public EasySimilarityProvider(EasySimilarity sim) {
+      this.sim = sim;
+    }
+    
+    @Override
+    public float coord(int overlap, int maxOverlap) {
+      return 1f;
+    }
+
+    @Override
+    public float queryNorm(float sumOfSquaredWeights) {
+      return 1f;
+    }
+
+    @Override
+    public EasySimilarity get(String field) {
+      return sim;
+    }
+  }
+}
