http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/conversation/testing/FakeConversationView.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/conversation/testing/FakeConversationView.java
 
b/wave/src/test/java/org/waveprotocol/wave/model/conversation/testing/FakeConversationView.java
new file mode 100644
index 0000000..85b2fd9
--- /dev/null
+++ 
b/wave/src/test/java/org/waveprotocol/wave/model/conversation/testing/FakeConversationView.java
@@ -0,0 +1,159 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.conversation.testing;
+
+import org.waveprotocol.wave.model.conversation.ObservableConversationView;
+import org.waveprotocol.wave.model.conversation.WaveBasedConversationView;
+import org.waveprotocol.wave.model.conversation.WaveletBasedConversation;
+import org.waveprotocol.wave.model.id.IdGenerator;
+import org.waveprotocol.wave.model.id.WaveId;
+import org.waveprotocol.wave.model.schema.SchemaProvider;
+import org.waveprotocol.wave.model.schema.conversation.ConversationSchemas;
+import org.waveprotocol.wave.model.testing.FakeIdGenerator;
+import org.waveprotocol.wave.model.testing.FakeWaveView;
+import org.waveprotocol.wave.model.wave.ParticipantId;
+import org.waveprotocol.wave.model.wave.data.DocumentFactory;
+import org.waveprotocol.wave.model.wave.opbased.ObservableWaveView;
+
+import java.util.Collection;
+
+/**
+ * A fake conversation view. The view is fully functioning but not attached to
+ * any communication channels.
+ *
+ * @author [email protected] (Alex North)
+ */
+public final class FakeConversationView implements ObservableConversationView {
+
+  private final static SchemaProvider DEFAULT_SCHEMAS = new 
ConversationSchemas();
+
+  public final static class Builder {
+    private SchemaProvider schemas;
+    private IdGenerator idGenerator;
+    private WaveId waveId;
+    private ParticipantId viewer;
+    private DocumentFactory<?> docFactory;
+
+    private Builder() {
+    }
+
+    public Builder with(DocumentFactory<?> docFactory) {
+      this.docFactory = docFactory;
+      return this;
+    }
+
+    public Builder with(SchemaProvider schemas) {
+      this.schemas = schemas;
+      return this;
+    }
+
+    public Builder with(IdGenerator idGenerator) {
+      this.idGenerator = idGenerator;
+      return this;
+    }
+
+    public Builder with(WaveId wid) {
+      this.waveId = wid;
+      return this;
+    }
+
+    public Builder with(ParticipantId viewer) {
+      this.viewer = viewer;
+      return this;
+    }
+
+    public FakeConversationView build() {
+      if (schemas == null) {
+        schemas = DEFAULT_SCHEMAS;
+      }
+      if (idGenerator == null) {
+        idGenerator = FakeIdGenerator.create();
+      }
+      if (waveId == null) {
+        waveId = idGenerator.newWaveId();
+      }
+
+      FakeWaveView waveView = FakeWaveView.builder(schemas) // \u2620
+          .with(docFactory) // \u2620
+          .with(idGenerator) // \u2620
+          .with(waveId) // \u2620
+          .with(viewer) // \u2620
+          .build();
+
+      return new 
FakeConversationView(WaveBasedConversationView.create(waveView, idGenerator));
+    }
+  }
+
+  /** Creates a new conversation view builder. */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** The backing conversation view. */
+  private final WaveBasedConversationView view;
+
+  private FakeConversationView(WaveBasedConversationView view) {
+    this.view = view;
+  }
+
+  @Override
+  public String getId() {
+    return view.getId();
+  }
+
+  @Override
+  public WaveletBasedConversation createConversation() {
+    return view.createConversation();
+  }
+
+  @Override
+  public WaveletBasedConversation createRoot() {
+    return view.createRoot();
+  }
+
+  @Override
+  public WaveletBasedConversation getConversation(String conversationId) {
+    return view.getConversation(conversationId);
+  }
+
+  @Override
+  public Collection<? extends WaveletBasedConversation> getConversations() {
+    return view.getConversations();
+  }
+
+  @Override
+  public WaveletBasedConversation getRoot() {
+    return view.getRoot();
+  }
+
+  @Override
+  public void addListener(Listener listener) {
+    view.addListener(listener);
+  }
+
+  @Override
+  public void removeListener(Listener listener) {
+    view.removeListener(listener);
+  }
+
+  public ObservableWaveView getWaveView() {
+    return view.getWaveView();
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/BasicFactories.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/BasicFactories.java 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/BasicFactories.java
new file mode 100644
index 0000000..f23e93b
--- /dev/null
+++ b/wave/src/test/java/org/waveprotocol/wave/model/testing/BasicFactories.java
@@ -0,0 +1,239 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+import org.waveprotocol.wave.model.document.Document;
+import org.waveprotocol.wave.model.document.ObservableDocument;
+import org.waveprotocol.wave.model.document.indexed.IndexedDocument;
+import org.waveprotocol.wave.model.document.operation.DocInitialization;
+import org.waveprotocol.wave.model.document.operation.DocOp;
+import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema;
+import org.waveprotocol.wave.model.document.raw.impl.Element;
+import org.waveprotocol.wave.model.document.raw.impl.Node;
+import org.waveprotocol.wave.model.document.raw.impl.Text;
+import org.waveprotocol.wave.model.document.util.DocProviders;
+import org.waveprotocol.wave.model.document.util.DocumentImpl;
+import org.waveprotocol.wave.model.document.util.DocumentProvider;
+import org.waveprotocol.wave.model.operation.SilentOperationSink;
+import org.waveprotocol.wave.model.schema.SchemaCollection;
+import org.waveprotocol.wave.model.schema.SchemaProvider;
+import org.waveprotocol.wave.model.util.Preconditions;
+import org.waveprotocol.wave.model.wave.data.DocumentFactory;
+import org.waveprotocol.wave.model.wave.data.MuteDocumentFactory;
+import 
org.waveprotocol.wave.model.wave.data.impl.ObservablePluggableMutableDocument;
+import org.waveprotocol.wave.model.wave.data.impl.PluggableMutableDocument;
+import org.waveprotocol.wave.model.wave.data.impl.WaveletDataImpl;
+
+import java.util.Map;
+
+/**
+ * Static factory for creating various document factories and builders whose
+ * document schemas come from the provider set using
+ * {@link #setSchemaProvider(SchemaProvider)}. If no provider is set the empty
+ * provider is used. This should only be used for tests of the core model.
+ *
+ */
+public class BasicFactories {
+
+  /**
+   * Provider of {@link Document}s based on the {@link DocProviders#POJO} DOM
+   * implementation.
+   */
+  private static final DocumentProvider<Document> DOC_PROVIDER = new 
DocumentProvider<Document>() {
+    @Override
+    public Document create(String tagName, Map<String, String> attributes) {
+      IndexedDocument<Node, Element, Text> doc = 
DocProviders.POJO.create(tagName, attributes);
+      return new DocumentImpl(DocProviders.createTrivialSequencer(doc), doc);
+    }
+
+    @Override
+    public Document parse(String text) {
+      IndexedDocument<Node, Element, Text> doc = DocProviders.POJO.parse(text);
+      return new DocumentImpl(DocProviders.createTrivialSequencer(doc), doc);
+    }
+  };
+
+  /**
+   * Provider of {@link ObservableDocument}s based on the
+   * {@link DocProviders#POJO} DOM implementation and a trivial sequence.
+   */
+  private static final DocumentProvider<ObservablePluggableMutableDocument> 
OBS_DOC_PROVIDER =
+      new DocumentProvider<ObservablePluggableMutableDocument>() {
+        @Override
+        public ObservablePluggableMutableDocument create(
+            String tagName, Map<String, String> attributes) {
+          // FIXME(ohler): this is inefficient.
+          return build(DocProviders.POJO.create(tagName, 
attributes).asOperation());
+        }
+
+        @Override
+        public ObservablePluggableMutableDocument parse(String text) {
+          // FIXME(ohler): this is inefficient.
+          return build(DocProviders.POJO.parse(text).asOperation());
+        }
+
+        private ObservablePluggableMutableDocument build(DocInitialization 
init) {
+          ObservablePluggableMutableDocument doc =
+              new 
ObservablePluggableMutableDocument(DocumentSchema.NO_SCHEMA_CONSTRAINTS, init);
+          doc.init(SilentOperationSink.VOID);
+          return doc;
+        }
+      };
+
+  private static SchemaProvider schemas = SchemaCollection.empty();
+
+  /**
+   * Sets the schema provider that will provide schemas for the factories
+   * returned from the methods of this class.
+   */
+  public static void setSchemaProvider(SchemaProvider value) {
+    schemas = value;
+  }
+
+  /**
+   * Returns the current schema provider.
+   */
+  protected static SchemaProvider getSchemas() {
+    return schemas;
+  }
+
+  /**
+   * Returns a new fake wave view builder whose document schemas comes from the
+   * current provider.
+   */
+  public static FakeWaveView.Builder fakeWaveViewBuilder() {
+    return FakeWaveView.builder(getSchemas());
+  }
+
+  /**
+   * Returns a new op-based wavelet factory builder whose document schemas 
comes
+   * from the current provider.
+   */
+  public static OpBasedWaveletFactory.Builder opBasedWaveletFactoryBuilder() {
+    return OpBasedWaveletFactory.builder(getSchemas());
+  }
+
+  /**
+   * Returns a new wavelet data impl factory whose document schemas comes from
+   * the current provider.
+   */
+  public static WaveletDataImpl.Factory waveletDataImplFactory() {
+    return 
WaveletDataImpl.Factory.create(observablePluggableMutableDocumentFactory());
+  }
+
+  /**
+   * Returns a mute document factory whose document schemas comes from the
+   * current provider.
+   */
+  public static MuteDocumentFactory muteDocumentFactory() {
+    return new MuteDocumentFactory(getSchemas());
+  }
+
+  /**
+   * Returns a fake document factory whose document schemas comes from the
+   * current provider.
+   */
+  public static FakeDocument.Factory fakeDocumentFactory() {
+    return FakeDocument.Factory.create(getSchemas());
+  }
+
+  /**
+   * Returns a plugable mutable document factory whose document schemas comes
+   * from the current provider.
+   */
+  public static DocumentFactory<? extends PluggableMutableDocument>
+      pluggableMutableDocumentFactory() {
+    return PluggableMutableDocument.createFactory(getSchemas());
+  }
+
+  /**
+   * Returns an observable pluggable mutable document factory whose document
+   * schemas comes from the current provider.
+   */
+  public static DocumentFactory<? extends ObservablePluggableMutableDocument>
+      observablePluggableMutableDocumentFactory() {
+    return ObservablePluggableMutableDocument.createFactory(getSchemas());
+  }
+
+  /**
+   * Returns a provider of {@link Document}s.
+   *
+   * Provided documents have no schema constraints: consider using
+   * {@link MuteDocumentFactory} instead.
+   *
+   * TODO(anorth): Remove this method in favor of one specifying a schema.
+   */
+  public static DocumentProvider<Document> documentProvider() {
+    return DOC_PROVIDER;
+  }
+
+  /**
+   * Returns a provider of observable mutable documents.
+   *
+   * Provided documents have no schema constraints: consider using
+   * {@link MuteDocumentFactory} instead.
+   *
+   *  TODO(anorth): Change generic type to ObservableDocument after fixing
+   * callers.
+   *
+   * TODO(anorth): Remove this method in favor of one specifying a schema.
+   */
+  public static DocumentProvider<ObservablePluggableMutableDocument> 
observableDocumentProvider() {
+    return OBS_DOC_PROVIDER;
+  }
+
+  /**
+   * Creates an observable mutable document with some schema, content, and 
sink.
+   */
+  public static ObservableDocument createDocument(DocumentSchema schema,
+      String initialContent, SilentOperationSink<? super DocOp> sink) {
+    Preconditions.checkNotNull(sink, "Sink can't be null");
+    DocInitialization init = 
DocProviders.POJO.parse(initialContent).asOperation();
+    ObservablePluggableMutableDocument doc = new 
ObservablePluggableMutableDocument(schema, init);
+    doc.init(sink);
+    return doc;
+  }
+
+  /**
+   * Creates an observable mutable document with some schema and a sink.
+   */
+  public static ObservableDocument createDocument(
+      DocumentSchema schema, SilentOperationSink<? super DocOp> sink) {
+    return createDocument(schema, "", sink);
+  }
+
+  /**
+   * Creates an observable mutable document with some schema.
+   */
+  public static ObservableDocument createDocument(DocumentSchema schema) {
+    return createDocument(schema, "", SilentOperationSink.VOID);
+  }
+
+  /**
+   * Creates an observable mutable document with some schema and initial 
content
+   */
+  public static ObservableDocument createDocument(
+      DocumentSchema schema, String initialContent) {
+    return createDocument(schema, initialContent, SilentOperationSink.VOID);
+  }
+
+  protected BasicFactories() {
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/DeltaTestUtil.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/DeltaTestUtil.java 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/DeltaTestUtil.java
new file mode 100755
index 0000000..2bd7021
--- /dev/null
+++ b/wave/src/test/java/org/waveprotocol/wave/model/testing/DeltaTestUtil.java
@@ -0,0 +1,199 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+import org.waveprotocol.wave.model.document.operation.DocOp;
+import org.waveprotocol.wave.model.document.operation.impl.DocOpBuilder;
+import org.waveprotocol.wave.model.operation.wave.AddParticipant;
+import org.waveprotocol.wave.model.operation.wave.BlipContentOperation;
+import org.waveprotocol.wave.model.operation.wave.NoOp;
+import org.waveprotocol.wave.model.operation.wave.RemoveParticipant;
+import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta;
+import org.waveprotocol.wave.model.operation.wave.WaveletBlipOperation;
+import org.waveprotocol.wave.model.operation.wave.WaveletDelta;
+import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
+import org.waveprotocol.wave.model.operation.wave.WaveletOperationContext;
+import org.waveprotocol.wave.model.util.CollectionUtils;
+import org.waveprotocol.wave.model.version.HashedVersion;
+import org.waveprotocol.wave.model.wave.Constants;
+import org.waveprotocol.wave.model.wave.ParticipantId;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+
+/**
+ * A bunch of utility functions to make testing easier.
+ *
+ * @author [email protected] (David Wang)
+ */
+public class DeltaTestUtil {
+  private static final WaveletOperationContext DUMMY = new 
WaveletOperationContext(null, 0L, 0L);
+
+  private final ParticipantId author;
+  private final Random random = new Random(42);
+
+  /**
+   * Creates a {@link DeltaTestUtil} with which operations authored by the 
given
+   * author can readily be made.
+   */
+  public DeltaTestUtil(String author) {
+    this(new ParticipantId(author));
+  }
+
+  /**
+   * Creates a {@link DeltaTestUtil} with which operations authored by the 
given
+   * author can readily be made.
+   */
+  public DeltaTestUtil(ParticipantId author) {
+    this.author = author;
+  }
+
+  public ParticipantId getAuthor() {
+    return author;
+  }
+
+  /**
+   * Creates an XmlDelete with the given data.
+   */
+  public WaveletOperation delete(int posStart, String characters, int 
remaining) {
+    DocOp op = new DocOpBuilder()
+        .retain(posStart)
+        .deleteCharacters(characters)
+        .retain(remaining)
+        .build();
+    BlipContentOperation blipOp = new BlipContentOperation(
+        new WaveletOperationContext(author, 0L, 1), op);
+    WaveletBlipOperation waveOp = new WaveletBlipOperation("blip id", blipOp);
+    return waveOp;
+  }
+
+  /**
+   * Wrap an op with a delta.
+   */
+  public TransformedWaveletDelta delta(long targetVersion, WaveletOperation 
op) {
+    return TransformedWaveletDelta.cloneOperations(author,
+        HashedVersion.unsigned(targetVersion + 1), 0L, Arrays.asList(op));
+  }
+
+  /**
+   * Create a delta with a single NoOp operation.
+   *
+   * @param initialVersion The version before the operation.
+   */
+  public TransformedWaveletDelta noOpDelta(long initialVersion) {
+    return makeTransformedDelta(0L, HashedVersion.unsigned(initialVersion + 
1), 1);
+  }
+
+  /** Create a NoOp operation. */
+  public NoOp noOp() {
+    return new NoOp(new WaveletOperationContext(author, 0L, 1L));
+  }
+
+  /** Create an AddParticipant operation. */
+  public AddParticipant addParticipant(ParticipantId participant) {
+    return new AddParticipant(new WaveletOperationContext(author, 0L, 1L), 
participant);
+  }
+
+  /** Creates a RemoveParticipant operation. */
+  public RemoveParticipant removeParticipant(ParticipantId participant) {
+    return new RemoveParticipant(new WaveletOperationContext(author, 0L, 1L), 
participant);
+  }
+
+  /**
+   * A docop that is empty. i.e. does nothing to the document. The document 
must
+   * also be empty, otherwise the operation is invalid.
+   */
+  public WaveletOperation noOpDocOp(String blipId) {
+    WaveletOperationContext context = new WaveletOperationContext(author, 0L, 
1L);
+    BlipContentOperation blipOp = new BlipContentOperation(context, (new 
DocOpBuilder()).build());
+    return new WaveletBlipOperation(blipId, blipOp);
+  }
+
+  /**
+   * Creates an XmlInsert with the given data.
+   */
+  public WaveletOperation insert(int pos, String text, int remaining,
+      HashedVersion resultingVersion) {
+    DocOpBuilder builder = new DocOpBuilder();
+    builder.retain(pos).characters(text);
+    if (remaining > 0) {
+      builder.retain(remaining);
+    }
+    BlipContentOperation blipOp = new BlipContentOperation(
+        new WaveletOperationContext(author, 0L, 1, resultingVersion), 
builder.build());
+    WaveletBlipOperation waveOp = new WaveletBlipOperation("blip id", blipOp);
+    return waveOp;
+  }
+
+  /**
+   * Builds a random client delta.
+   */
+  public WaveletDelta makeDelta(HashedVersion targetVersion, long timestamp, 
int numOps) {
+    List<WaveletOperation> ops = CollectionUtils.newArrayList();
+    WaveletOperationContext context =
+        new WaveletOperationContext(author, Constants.NO_TIMESTAMP, 1);
+    for (int i = 0; i < numOps; ++i) {
+      ops.add(randomOp(context));
+    }
+    return new WaveletDelta(author, targetVersion, ops);
+  }
+
+  /**
+   * Builds a no-op client delta.
+   */
+  public WaveletDelta makeNoOpDelta(HashedVersion targetVersion, long 
timestamp, int numOps) {
+    List<WaveletOperation> ops = CollectionUtils.newArrayList();
+    WaveletOperationContext context =
+        new WaveletOperationContext(author, Constants.NO_TIMESTAMP, 1);
+    for (int i = 0; i < numOps; ++i) {
+      ops.add(new NoOp(context));
+    }
+    return new WaveletDelta(author, targetVersion, ops);
+  }
+
+  /**
+   * Builds a random transformed delta.
+   */
+  public TransformedWaveletDelta makeTransformedDelta(long 
applicationTimestamp,
+      HashedVersion resultingVersion, int numOps) {
+    List<WaveletOperation> ops = CollectionUtils.newArrayList();
+    for (int i = 0; i < numOps; ++i) {
+      ops.add(randomOp(DUMMY));
+    }
+    return TransformedWaveletDelta.cloneOperations(author, resultingVersion, 
applicationTimestamp,
+        ops);
+  }
+
+  /**
+   * Creates a random op. The result is unlikely to be applicable to any
+   * wavelet, but is generated such that we are fairly certain that it will be
+   * unique so we can identify it when it completes a round-trip.
+   */
+  private WaveletOperation randomOp(WaveletOperationContext context) {
+    DocOp blipOp = new DocOpBuilder()
+        .retain(Math.abs(random.nextInt()) / 2 + 1)
+        .characters("createRndOp#" + random.nextInt())
+        .build();
+    return new WaveletBlipOperation("createRndId#" + random.nextInt(),
+        new BlipContentOperation(context, blipOp));
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/DocOpCreator.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/DocOpCreator.java 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/DocOpCreator.java
new file mode 100644
index 0000000..31f589b
--- /dev/null
+++ b/wave/src/test/java/org/waveprotocol/wave/model/testing/DocOpCreator.java
@@ -0,0 +1,266 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+import org.waveprotocol.wave.model.document.operation.Attributes;
+import org.waveprotocol.wave.model.document.operation.AttributesUpdate;
+import org.waveprotocol.wave.model.document.operation.DocOp;
+import org.waveprotocol.wave.model.document.operation.EvaluatingDocOpCursor;
+import 
org.waveprotocol.wave.model.document.operation.impl.AnnotationBoundaryMapImpl;
+import 
org.waveprotocol.wave.model.document.operation.impl.AttributesUpdateImpl;
+import org.waveprotocol.wave.model.document.operation.impl.DocOpBuffer;
+import org.waveprotocol.wave.model.util.Preconditions;
+
+/**
+ * A convenience class for creating document operations.
+ *
+ */
+public class DocOpCreator {
+
+  /**
+   * A builder for BufferedDocOps which is used by the static convenience
+   * methods of DocOpCreator for creating operations. This builder allows
+   * calling "retain" with an argument of 0 and "characters" and
+   * "deleteCharacters" with an empty string argument, in order to make the
+   * building process easier in some circumstances.
+   */
+  private static class SimplifyingDocOpBuilder {
+
+    private final EvaluatingDocOpCursor<DocOp> buffer = new DocOpBuffer();
+
+    public final DocOp build() {
+      return buffer.finish();
+    }
+
+    public final SimplifyingDocOpBuilder retain(int itemCount) {
+      Preconditions.checkArgument(itemCount >= 0, "Negative item count");
+      if (itemCount > 0) {
+        buffer.retain(itemCount);
+      }
+      return this;
+    }
+
+    public final SimplifyingDocOpBuilder characters(String characters) {
+      if (characters.length() > 0) {
+        buffer.characters(characters);
+      }
+      return this;
+    }
+
+    public final SimplifyingDocOpBuilder elementStart(String type, Attributes 
attrs) {
+      buffer.elementStart(type, attrs);
+      return this;
+    }
+
+    public final SimplifyingDocOpBuilder elementEnd() {
+      buffer.elementEnd();
+      return this;
+    }
+
+    public final SimplifyingDocOpBuilder deleteCharacters(String characters) {
+      if (characters.length() > 0) {
+        buffer.deleteCharacters(characters);
+      }
+      return this;
+    }
+
+    public final SimplifyingDocOpBuilder deleteElementStart(String type, 
Attributes attrs) {
+      buffer.deleteElementStart(type, attrs);
+      return this;
+    }
+
+    public final SimplifyingDocOpBuilder deleteElementEnd() {
+      buffer.deleteElementEnd();
+      return this;
+    }
+
+    public final SimplifyingDocOpBuilder replaceAttributes(Attributes oldAttrs,
+        Attributes newAttrs) {
+      buffer.replaceAttributes(oldAttrs, newAttrs);
+      return this;
+    }
+
+    public final SimplifyingDocOpBuilder updateAttributes(AttributesUpdate 
update) {
+      buffer.updateAttributes(update);
+      return this;
+    }
+
+    public final SimplifyingDocOpBuilder setAnnotation(int itemCount, String 
key, String oldValue,
+        String newValue) {
+      Preconditions.checkArgument(itemCount >= 0, "Negative item count");
+      if (itemCount > 0) {
+        buffer.annotationBoundary(AnnotationBoundaryMapImpl.builder()
+            .updateValues(key, oldValue, newValue)
+            .build());
+        buffer.retain(itemCount);
+        buffer.annotationBoundary(AnnotationBoundaryMapImpl.builder()
+            .initializationEnd(key)
+            .build());
+      }
+      return this;
+    }
+
+  }
+
+  /**
+   * Creates a document operation that inserts the given characters at the 
given
+   * location.
+   *
+   * @param size The initial size of the document.
+   * @param location The location at which to insert characters.
+   * @param characters The characters to insert.
+   * @return The document operation.
+   */
+  public static DocOp insertCharacters(int size, int location, String 
characters) {
+    return new SimplifyingDocOpBuilder()
+        .retain(location)
+        .characters(characters)
+        .retain(size - location)
+        .build();
+  }
+
+  /**
+   * Creates a document operation that inserts an element at the given 
location.
+   *
+   * @param size The initial size of the document.
+   * @param location The location at which to insert the element.
+   * @param type The type of the element.
+   * @param attributes The attributes of the element.
+   * @return The document operation.
+   */
+  public static DocOp insertElement(int size, int location, String type,
+      Attributes attributes) {
+    return new SimplifyingDocOpBuilder()
+        .retain(location)
+        .elementStart(type, attributes)
+        .elementEnd()
+        .retain(size - location)
+        .build();
+  }
+
+  /**
+   * Creates a document operation that deletes the characters denoted by the
+   * given range.
+   *
+   * @param size The initial size of the document.
+   * @param location The location the characters to delete.
+   * @param characters The characters to delete.
+   * @return The document operation.
+   */
+  public static DocOp deleteCharacters(int size, int location, String 
characters) {
+    return new SimplifyingDocOpBuilder()
+        .retain(location)
+        .deleteCharacters(characters)
+        .retain(size - location - characters.length())
+        .build();
+  }
+
+  /**
+   * Creates a document operation that deletes an empty element at a given
+   * location.
+   *
+   * @param size The initial size of the document.
+   * @param location The location of the element to delete.
+   * @param type The type of the element.
+   * @param attributes The attributes of the element.
+   * @return The document operation.
+   */
+  public static DocOp deleteElement(int size, int location, String type,
+      Attributes attributes) {
+    return new SimplifyingDocOpBuilder()
+        .retain(location)
+        .deleteElementStart(type, attributes)
+        .deleteElementEnd()
+        .retain(size - location - 2)
+        .build();
+  }
+
+  /**
+   * Creates a document operation that replace all the attributes of an 
element.
+   *
+   * @param size The initial size of the document.
+   * @param location The location of the element whose attributes are to be 
set.
+   * @param oldAttr The old attributes of the element.
+   * @param newAttr The new attributes that the element should have.
+   * @return The document operation.
+   */
+  public static DocOp replaceAttributes(int size, int location, Attributes 
oldAttr,
+      Attributes newAttr) {
+    return new SimplifyingDocOpBuilder()
+        .retain(location)
+        .replaceAttributes(oldAttr, newAttr)
+        .retain(size - location - 1)
+        .build();
+  }
+
+  /**
+   * Creates a document operation that sets an attribute of an element.
+   *
+   * @param size The initial size of the document.
+   * @param location The location of the element whose attribute is to be set.
+   * @param name The name of the attribute to set.
+   * @param oldValue The old value of the attribute.
+   * @param newValue The value to which to set the attribute.
+   * @return The document operation.
+   */
+  public static DocOp setAttribute(int size, int location, String name, String 
oldValue,
+      String newValue) {
+    return new SimplifyingDocOpBuilder()
+        .retain(location)
+        .updateAttributes(new AttributesUpdateImpl(name, oldValue, newValue))
+        .retain(size - location - 1)
+        .build();
+  }
+
+  /**
+   * Creates a document operation that sets an annotation over a range.
+   *
+   * @param size The initial size of the document.
+   * @param start The location of the start of the range on which the 
annotation
+   *        is to be set.
+   * @param end The location of the end of the range on which the annotation is
+   *        to be set.
+   * @param key The annotation key.
+   * @param oldValue The old annotation value.
+   * @param newValue The new annotation value.
+   * @return The document operation.
+   */
+  public static DocOp setAnnotation(int size, int start, int end, String key,
+      String oldValue, String newValue) {
+    return new SimplifyingDocOpBuilder()
+        .retain(start)
+        .setAnnotation(end - start, key, oldValue, newValue)
+        .retain(size - end)
+        .build();
+  }
+
+  /**
+   * Creates a document operation that acts as the identity on a document.
+   *
+   * @param size The size of the document.
+   * @return The document operation.
+   */
+  public static DocOp identity(int size) {
+    return new SimplifyingDocOpBuilder()
+        .retain(size)
+        .build();
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/ExtraAsserts.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/ExtraAsserts.java 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/ExtraAsserts.java
new file mode 100644
index 0000000..989240b
--- /dev/null
+++ b/wave/src/test/java/org/waveprotocol/wave/model/testing/ExtraAsserts.java
@@ -0,0 +1,92 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+import org.waveprotocol.wave.model.wave.data.BlipData;
+
+import junit.framework.Assert;
+
+import org.waveprotocol.wave.model.document.MutableDocument;
+import org.waveprotocol.wave.model.document.ReadableWDocument;
+import org.waveprotocol.wave.model.document.operation.impl.DocOpUtil;
+import org.waveprotocol.wave.model.document.util.DocCompare;
+import org.waveprotocol.wave.model.document.util.XmlStringBuilder;
+
+/**
+ * Extra assertions that are useful for tests involving the model.
+ *
+ */
+public final class ExtraAsserts {
+
+  /**
+   * Asserts that the structure of the document and the builder are the same.
+   */
+  public static <N, E extends N, T extends N> void assertStructureEquivalent(
+      XmlStringBuilder expected, MutableDocument<N, E, T> doc) {
+    String expectedStr = expected.getXmlString();
+    if (!DocCompare.equivalent(DocCompare.STRUCTURE, expectedStr, doc)) {
+      String docStr = doc.toXmlString();
+      String message = "Expected [" + expectedStr + "], found [" + docStr + 
"]";
+      Assert.fail(message);
+    }
+  }
+
+  /**
+   * Asserts that the structure of the two documents are the same.
+   */
+  public static <N1, N2> void assertStructureEquivalent(ReadableWDocument<N1, 
?, ?> doc1,
+      ReadableWDocument<N2, ?, ?> doc2) {
+    if (!DocCompare.equivalent(DocCompare.STRUCTURE, doc1, doc2)) {
+      String doc1Str = doc1.toXmlString();
+      String doc2Str = doc2.toXmlString();
+      String message = "Expected [" + doc1Str + "] found [" + doc2Str + "]";
+      Assert.fail(message);
+    }
+  }
+
+  /**
+   * Asserts that the content, both structure and annotations, of the document
+   * and the builder are the same.
+   */
+  public static <N, E extends N, T extends N> void assertEqual(
+      XmlStringBuilder expected, MutableDocument<N, E, T> doc) {
+    String expectedStr = expected.getXmlString();
+    if (!DocCompare.equivalent(DocCompare.ALL, expectedStr, doc)) {
+      String docStr = doc.toXmlString();
+      String message = "Expected [" + expectedStr + "], found [" + docStr + 
"]";
+      Assert.fail(message);
+    }
+  }
+
+  // Static utility class
+  private ExtraAsserts() { }
+
+  /**
+   * Checks the content of a document and asserts it matches the given expected
+   * content.
+   *
+   * @param expectedContent The expected content.
+   * @param root The content to check.
+   */
+  public static void checkContent(String expectedContent, BlipData root) {
+    Assert.assertEquals(expectedContent, 
DocOpUtil.toXmlString(root.getContent().asOperation()));
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/Factory.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/Factory.java 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/Factory.java
new file mode 100644
index 0000000..7ae8f7a
--- /dev/null
+++ b/wave/src/test/java/org/waveprotocol/wave/model/testing/Factory.java
@@ -0,0 +1,34 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+/**
+ * Generic factory interface.  The intended use within this test package is
+ * to allow black-box tests, which only test an interface, to be decoupled from
+ * the construction of the particular instance of that interface to test.
+ *
+ * @param <T> type of created instances
+ */
+public interface Factory<T> {
+  /**
+   * Creates an instance.
+   */
+  T create();
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeDocument.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeDocument.java 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeDocument.java
new file mode 100644
index 0000000..33de8c1
--- /dev/null
+++ b/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeDocument.java
@@ -0,0 +1,81 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+import org.waveprotocol.wave.model.document.operation.DocInitialization;
+import org.waveprotocol.wave.model.document.operation.DocOp;
+import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema;
+import org.waveprotocol.wave.model.id.WaveletId;
+import org.waveprotocol.wave.model.operation.OperationException;
+import org.waveprotocol.wave.model.schema.SchemaProvider;
+import org.waveprotocol.wave.model.wave.data.DocumentFactory;
+import 
org.waveprotocol.wave.model.wave.data.impl.ObservablePluggableMutableDocument;
+
+/**
+ * A document implementation and factory for use in tests.
+ *
+ */
+public class FakeDocument extends ObservablePluggableMutableDocument {
+
+  public static class Factory implements DocumentFactory<FakeDocument> {
+
+    private final SchemaProvider schemas;
+
+    public static Factory create(SchemaProvider schemas) {
+      return new Factory(schemas);
+    }
+
+    private Factory(SchemaProvider schemas) {
+      this.schemas = schemas;
+    }
+
+    private DocumentSchema getSchemaForId(WaveletId waveletId, String 
documentId) {
+      DocumentSchema result = schemas.getSchemaForId(waveletId, documentId);
+      return (result != null) ? result : DocumentSchema.NO_SCHEMA_CONSTRAINTS;
+    }
+
+    @Override
+    public FakeDocument create(final WaveletId waveletId, final String blipId,
+        DocInitialization content) {
+      return new FakeDocument(content, getSchemaForId(waveletId, blipId));
+    }
+  }
+
+  private DocOp consumed;
+
+  public FakeDocument(DocInitialization initial, DocumentSchema schema) {
+    super(schema, initial);
+  }
+
+  @Override
+  public void consume(DocOp op) throws OperationException {
+    super.consume(op);
+    this.consumed = op;
+  }
+
+  public DocOp getConsumed() {
+    return consumed;
+  }
+
+  @Override
+  public String toString() {
+    return toXmlString();
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeHashedVersionFactory.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeHashedVersionFactory.java
 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeHashedVersionFactory.java
new file mode 100644
index 0000000..e9d25b8
--- /dev/null
+++ 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeHashedVersionFactory.java
@@ -0,0 +1,45 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+import org.waveprotocol.wave.model.id.WaveletName;
+import org.waveprotocol.wave.model.version.HashedVersion;
+import org.waveprotocol.wave.model.version.HashedVersionFactory;
+
+/**
+ * A hashed version factory which generates unsigned versions.
+ *
+ * @author [email protected] (Alex North)
+ */
+public final class FakeHashedVersionFactory implements HashedVersionFactory {
+
+  public static final HashedVersionFactory INSTANCE = new 
FakeHashedVersionFactory();
+
+  @Override
+  public HashedVersion createVersionZero(WaveletName waveletName) {
+    return HashedVersion.unsigned(0);
+  }
+
+  @Override
+  public HashedVersion create(byte[] appliedDeltaBytes, HashedVersion 
versionAppliedAt,
+      int opsApplied) {
+    return HashedVersion.unsigned(versionAppliedAt.getVersion() + opsApplied);
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeIdGenerator.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeIdGenerator.java 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeIdGenerator.java
new file mode 100644
index 0000000..f46802c
--- /dev/null
+++ 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeIdGenerator.java
@@ -0,0 +1,44 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+import org.waveprotocol.wave.model.id.IdGenerator;
+import org.waveprotocol.wave.model.id.IdGeneratorImpl;
+import org.waveprotocol.wave.model.id.IdGeneratorImpl.Seed;
+
+
+/**
+ * Id generator suitable for use in testing.
+ *
+ */
+public final class FakeIdGenerator {
+
+  // Prevent instantiation
+  private FakeIdGenerator() {}
+
+  public static IdGenerator create() {
+    return new IdGeneratorImpl("example.com", new Seed() {
+      @Override
+      public String get() {
+        return "seed";
+      }
+    });
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeSilentOperationSink.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeSilentOperationSink.java
 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeSilentOperationSink.java
new file mode 100644
index 0000000..43bbd34
--- /dev/null
+++ 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeSilentOperationSink.java
@@ -0,0 +1,69 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+import org.waveprotocol.wave.model.operation.Operation;
+import org.waveprotocol.wave.model.operation.SilentOperationSink;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A place where you can get a concrete OperationSink.Silent for testing.
+ *
+ * @author [email protected] (David Wang)
+ */
+public class FakeSilentOperationSink<T extends Operation<?>> implements 
SilentOperationSink<T> {
+  private LinkedList<T> ops = new LinkedList<T>();
+
+  /**
+   * For unit testing
+   * @return the most recently consumed op
+   */
+  public T getConsumedOp() {
+    int size = ops.size();
+    return (size == 0) ? null : (ops.get(size - 1));
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public void consume(T op) {
+    ops.addLast(op);
+  }
+
+  /**
+   * Clears the list of saved operations.
+   */
+  public void clear() {
+    ops.clear();
+  }
+
+  /**
+   * Gets the list of operations consumed by this sink since it was last
+   * cleared.
+   *
+   * @return the ops, from first consumed through most recently consumed.
+   */
+  public List<T> getOps() {
+    return Collections.unmodifiableList(ops);
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeWaveView.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeWaveView.java 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeWaveView.java
new file mode 100644
index 0000000..5f74e11
--- /dev/null
+++ b/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeWaveView.java
@@ -0,0 +1,229 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+import org.waveprotocol.wave.model.id.IdGenerator;
+import org.waveprotocol.wave.model.id.WaveId;
+import org.waveprotocol.wave.model.id.WaveletId;
+import org.waveprotocol.wave.model.operation.SilentOperationSink;
+import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
+import org.waveprotocol.wave.model.operation.wave.WaveletOperationContext;
+import org.waveprotocol.wave.model.schema.SchemaProvider;
+import org.waveprotocol.wave.model.wave.ObservableWavelet;
+import org.waveprotocol.wave.model.wave.ParticipantId;
+import org.waveprotocol.wave.model.wave.WaveViewListener;
+import org.waveprotocol.wave.model.wave.data.DocumentFactory;
+import org.waveprotocol.wave.model.wave.data.impl.WaveletDataImpl;
+import org.waveprotocol.wave.model.wave.opbased.ObservableWaveView;
+import org.waveprotocol.wave.model.wave.opbased.OpBasedWavelet;
+import org.waveprotocol.wave.model.wave.opbased.WaveViewImpl;
+
+/**
+ * Dummy implementation of a wave view.
+ *
+ */
+public final class FakeWaveView implements ObservableWaveView, 
Factory<OpBasedWavelet> {
+
+  public final static class Builder {
+    private final SchemaProvider schemas;
+    private IdGenerator idGenerator;
+    private WaveId waveId;
+    private ParticipantId viewer;
+    private SilentOperationSink<? super WaveletOperation> sink;
+    private WaveViewImpl.WaveletConfigurator configurator;
+    private DocumentFactory<?> docFactory;
+
+    private Builder(SchemaProvider schemas) {
+      this.schemas = schemas;
+    }
+
+    public Builder with(DocumentFactory<?> docFactory) {
+      this.docFactory = docFactory;
+      return this;
+    }
+
+    public Builder with(IdGenerator idGenerator) {
+      this.idGenerator = idGenerator;
+      return this;
+    }
+
+    public Builder with(WaveId wid) {
+      this.waveId = wid;
+      return this;
+    }
+
+    public Builder with(ParticipantId viewer) {
+      this.viewer = viewer;
+      return this;
+    }
+
+    public Builder with(SilentOperationSink<? super WaveletOperation> sink) {
+      this.sink = sink;
+      return this;
+    }
+
+    public Builder with(WaveViewImpl.WaveletConfigurator configurator) {
+      this.configurator = configurator;
+      return this;
+    }
+
+    public FakeWaveView build() {
+      if (idGenerator == null) {
+        idGenerator = FakeIdGenerator.create();
+      }
+      if (waveId == null) {
+        waveId = idGenerator.newWaveId();
+      }
+      if (viewer == null) {
+        viewer = FAKE_PARTICIPANT;
+      }
+      if (sink == null) {
+        sink = SilentOperationSink.VOID;
+      }
+      if (configurator == null) {
+        configurator = WaveViewImpl.WaveletConfigurator.ADD_CREATOR;
+      }
+      if (docFactory == null) {
+        // Document factory that accepts output-sink registrations.
+        docFactory = FakeDocument.Factory.create(schemas);
+      }
+
+      // Wavelet factory that does all the work.
+      OpBasedWaveletFactory waveletFactory = OpBasedWaveletFactory // \u2620
+          .builder(schemas) // \u2620
+          .with(WaveletDataImpl.Factory.create(docFactory)) // \u2620
+          .with(sink) // \u2620
+          .with(viewer) // \u2620
+          .build();
+
+      // And the view implementation using that factory.
+      WaveViewImpl<OpBasedWavelet> view =
+          WaveViewImpl.create(waveletFactory, waveId, idGenerator, viewer, 
configurator);
+
+      return new FakeWaveView(waveletFactory, view);
+    }
+  }
+
+  private static final ParticipantId FAKE_PARTICIPANT = new 
ParticipantId("[email protected]");
+
+  private final OpBasedWaveletFactory factory;
+  private final WaveViewImpl<? extends OpBasedWavelet> view;
+
+  /**
+   * Creates a wave view.
+   *
+   * @param factory  factory exposing testing hacks
+   * @param view     real view implementation
+   */
+  private FakeWaveView(OpBasedWaveletFactory factory, WaveViewImpl<? extends 
OpBasedWavelet> view) {
+    this.factory = factory;
+    this.view = view;
+  }
+
+  /**
+   * @return a builder for a fake wave view.
+   */
+  public static Builder builder(SchemaProvider schemas) {
+    return new Builder(schemas);
+  }
+
+  //
+  // Expose as basic wavelet factory for wavelet-specific tests.
+  //
+
+  @Override
+  public OpBasedWavelet create() {
+    return createWavelet();
+  }
+
+  //
+  // Testing hacks.
+  //
+
+  public MockParticipationHelper getLastAuthoriser() {
+    return factory.getLastAuthoriser();
+  }
+
+  public WaveletOperationContext.Factory getLastContextFactory() {
+    return factory.getLastContextFactory();
+  }
+
+  public OpBasedWavelet createWavelet(WaveletId id) {
+    return view.createWavelet(id);
+  }
+
+  public void removeWavelet(ObservableWavelet wavelet) {
+    view.removeWavelet(wavelet);
+  }
+
+  //
+  // Delegate view implementation to view.
+  //
+
+  @Override
+  public OpBasedWavelet createRoot() {
+    return view.createRoot();
+  }
+
+  @Override
+  public OpBasedWavelet createUserData() {
+    return view.createUserData();
+  }
+
+  @Override
+  public OpBasedWavelet createWavelet() {
+    return view.createWavelet();
+  }
+
+  @Override
+  public OpBasedWavelet getRoot() {
+    return view.getRoot();
+  }
+
+  @Override
+  public OpBasedWavelet getUserData() {
+    return view.getUserData();
+  }
+
+  @Override
+  public OpBasedWavelet getWavelet(WaveletId waveletId) {
+    return view.getWavelet(waveletId);
+  }
+
+  @Override
+  public Iterable<? extends OpBasedWavelet> getWavelets() {
+    return view.getWavelets();
+  }
+
+  @Override
+  public WaveId getWaveId() {
+    return view.getWaveId();
+  }
+
+  @Override
+  public void addListener(WaveViewListener listener) {
+    view.addListener(listener);
+  }
+
+  @Override
+  public void removeListener(WaveViewListener listener) {
+    view.removeListener(listener);
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeWaveletDataListener.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeWaveletDataListener.java
 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeWaveletDataListener.java
new file mode 100644
index 0000000..b281a10
--- /dev/null
+++ 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeWaveletDataListener.java
@@ -0,0 +1,320 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+import org.waveprotocol.wave.model.version.HashedVersion;
+import org.waveprotocol.wave.model.wave.ParticipantId;
+import org.waveprotocol.wave.model.wave.data.BlipData;
+import org.waveprotocol.wave.model.wave.data.WaveletData;
+import org.waveprotocol.wave.model.wave.data.WaveletDataListener;
+
+/**
+ * Stub implementation of {@link WaveletDataListener}. Each notification method
+ * saves the passed parameters for later inspection by accessors.
+ *
+ * @author [email protected] (David Wang)
+ */
+public class FakeWaveletDataListener implements WaveletDataListener {
+  /**
+   * The last participantId received from
+   * {@link #onParticipantAdded(WaveletData, ParticipantId)}
+   */
+  private ParticipantId participantAdded;
+
+  /**
+   * The last participantId received from
+   * {@link #onParticipantRemoved(WaveletData, ParticipantId)}
+   */
+  private ParticipantId participantRemoved;
+
+  /**
+   * The last blip received from {@link #onBlipDataAdded(WaveletData, 
BlipData)}
+   */
+  private BlipData blipDataAdded;
+
+  /**
+   * The last oldTitle received from {@link #onTitleChanged(WaveletData, 
String, String)}.
+   */
+  private String oldTitle;
+
+  /**
+   * The last newTitle received from {@link #onTitleChanged(WaveletData, 
String, String)}.
+   */
+  private String newTitle;
+
+  /**
+   * The last blip target received from any other onBlipXxx method.
+   */
+  private BlipData blipModified;
+
+  /**
+   * The last old modified time received by
+   * {@link #onLastModifiedTimeChanged(WaveletData, long, long)}
+   */
+  private long oldLastModifiedTime;
+
+  /**
+   * The last new modified time received by
+   * {@link #onLastModifiedTimeChanged(WaveletData, long, long)}
+   */
+  private long newLastModifiedTime;
+
+  /**
+   * The last contributor received by
+   * {@link #onBlipDataContributorAdded(WaveletData, BlipData, ParticipantId)}
+   */
+  private ParticipantId blipContributorAdded;
+
+  /**
+   * The last contributor received by
+   * {@link #onBlipDataContributorRemoved(WaveletData, BlipData, 
ParticipantId)}
+   */
+  private ParticipantId blipContributorRemoved;
+
+  /**
+   * The last old timestamp received by
+   * {@link #onBlipDataTimestampModified(WaveletData, BlipData, long, long)}
+   */
+  private long blipOldTimestamp;
+
+  /**
+   * The last new timestamp received by
+   * {@link #onBlipDataTimestampModified(WaveletData, BlipData, long, long)}
+   */
+  private long blipNewTimestamp;
+  /**
+   * The last old version received by
+   * {@link #onBlipDataVersionModified(WaveletData, BlipData, long, long)}
+   */
+  private long blipOldVersion;
+
+  /**
+   * The last new version received by
+   * {@link #onBlipDataVersionModified(WaveletData, BlipData, long, long)}
+   */
+  private long blipNewVersion;
+
+  private long oldVersion;
+  private long newVersion;
+
+  private HashedVersion oldHashedVersion;
+  private HashedVersion newHashedVersion;
+
+  @Override
+  public void onParticipantAdded(WaveletData wavelet, ParticipantId 
participantId) {
+    this.participantAdded = participantId;
+  }
+
+  @Override
+  public void onParticipantRemoved(WaveletData wavelet, ParticipantId 
participantId) {
+    this.participantRemoved = participantId;
+  }
+
+  @Override
+  public void onLastModifiedTimeChanged(WaveletData waveletData, long oldTime, 
long newTime) {
+    this.oldLastModifiedTime = oldTime;
+    this.newLastModifiedTime = newTime;
+  }
+
+  @Override
+  public void onVersionChanged(WaveletData wavelet, long oldVersion, long 
newVersion) {
+    this.oldVersion = oldVersion;
+    this.newVersion = newVersion;
+  }
+
+  @Override
+  public void onHashedVersionChanged(WaveletData waveletData, HashedVersion 
oldHashedVersion,
+      HashedVersion newHashedVersion) {
+    this.oldHashedVersion = oldHashedVersion;
+    this.newHashedVersion = newHashedVersion;
+  }
+
+  @Override
+  public void onBlipDataAdded(WaveletData waveletData, BlipData blip) {
+    this.blipDataAdded = blip;
+  }
+
+  @Override
+  public void onBlipDataContributorAdded(
+      WaveletData waveletData, BlipData blip, ParticipantId contributor) {
+    this.blipModified = blip;
+    this.blipContributorAdded = contributor;
+  }
+
+  @Override
+  public void onBlipDataContributorRemoved(
+      WaveletData waveletData, BlipData blip, ParticipantId contributor) {
+    this.blipModified = blip;
+    this.blipContributorRemoved = contributor;
+  }
+
+  @Override
+  public void onBlipDataTimestampModified(
+      WaveletData waveletData, BlipData blip, long oldTime, long newTime) {
+    this.blipModified = blip;
+    this.blipOldTimestamp = oldTime;
+    this.blipNewTimestamp = newTime;
+  }
+
+  @Override
+  public void onBlipDataVersionModified(
+      WaveletData waveletData, BlipData blip, long oldVersion, long 
newVersion) {
+    this.blipModified = blip;
+    this.blipOldVersion = oldVersion;
+    this.blipNewVersion = newVersion;
+  }
+
+  @Deprecated
+  @Override
+  public void onRemoteBlipDataContentModified(WaveletData waveletData, 
BlipData blip) {
+    this.blipModified = blip;
+  }
+
+  @Override
+  public void onBlipDataSubmitted(WaveletData waveletData, BlipData blip) {
+    this.blipModified = blip;
+  }
+
+  /**
+   * @return the last participantId received by
+   *         {@link #onParticipantAdded(WaveletData, ParticipantId)}
+   */
+  public ParticipantId getParticipantAdded() {
+    return participantAdded;
+  }
+
+  /**
+   * @return the last participantId received by
+   *         {@link #onParticipantRemoved(WaveletData, ParticipantId)}
+   */
+  public ParticipantId getParticipantRemoved() {
+    return participantRemoved;
+  }
+
+  /**
+   * @return the last blip received by {@link #onBlipDataAdded(WaveletData, 
BlipData)}
+   */
+  public BlipData getBlipDataAdded() {
+    return blipDataAdded;
+  }
+
+  /**
+   * @return the last blip received by any of the other onBlipDataXxx methods.
+   */
+  public BlipData getBlipModified() {
+    return blipModified;
+  }
+
+  /**
+   * @return the last newTitle received by
+   *         {@link #onTitleChanged(WaveletData, String, String)}.
+   */
+  public String getNewTitle() {
+    return newTitle;
+  }
+
+  /**
+   * @return the last oldTitle received by
+   *         {@link #onTitleChanged(WaveletData, String, String)}.
+   */
+  public String getOldTitle() {
+    return oldTitle;
+  }
+
+  /**
+   * @return the last old time received by
+   *         {@link #onLastModifiedTimeChanged(WaveletData, long, long)}
+   */
+  public long getOldLastModifiedTime() {
+    return oldLastModifiedTime;
+  }
+
+  /**
+   * @return the last new time received by
+   *         {@link #onLastModifiedTimeChanged(WaveletData, long, long)}
+   */
+  public long getNewLastModifiedTime() {
+    return newLastModifiedTime;
+  }
+
+  /**
+   * @return the last participant received by
+   *         {@link #onBlipDataContributorAdded(WaveletData, BlipData, 
ParticipantId)}
+   */
+  public ParticipantId getBlipContributorAdded() {
+    return blipContributorAdded;
+  }
+
+  /**
+   * @return the last participant receieved by
+   *         {@link #onBlipDataContributorRemoved(WaveletData, BlipData, 
ParticipantId)}
+   */
+  public ParticipantId getBlipContributorRemoved() {
+    return blipContributorRemoved;
+  }
+
+  /**
+   * @return the last old timestamp received by
+   *         {@link #onBlipDataTimestampModified(WaveletData, BlipData, long, 
long)}
+   */
+  public long getBlipOldTimestamp() {
+    return blipOldTimestamp;
+  }
+
+  /**
+   * @return the last new timestamp received by
+   *         {@link #onBlipDataTimestampModified(WaveletData, BlipData, long, 
long)}
+   */
+  public long getBlipNewTimestamp() {
+    return blipNewTimestamp;
+  }
+
+  /**
+   * @return the last new version received by
+   *         {@link #onBlipDataVersionModified(WaveletData, BlipData, long, 
long)}
+   */
+  public long getBlipOldVersion() {
+    return blipOldVersion;
+  }
+
+  /**
+   * @return the last old version received by
+   *         {@link #onBlipDataVersionModified(WaveletData, BlipData, long, 
long)}
+   */
+  public long getBlipNewVersion() {
+    return blipNewVersion;
+  }
+
+  public long getNewVersion() {
+    return newVersion;
+  }
+
+  public long getOldVersion() {
+    return oldVersion;
+  }
+
+  public HashedVersion getNewHashedVersion() {
+    return newHashedVersion;
+  }
+
+  public HashedVersion getOldHashedVersion() {
+    return oldHashedVersion;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeWaveletListener.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeWaveletListener.java
 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeWaveletListener.java
new file mode 100644
index 0000000..8d66614
--- /dev/null
+++ 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/FakeWaveletListener.java
@@ -0,0 +1,58 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+import org.waveprotocol.wave.model.wave.ObservableWavelet;
+import org.waveprotocol.wave.model.wave.WaveletListener;
+import org.waveprotocol.wave.model.wave.opbased.WaveletListenerImpl;
+
+import org.waveprotocol.wave.model.wave.ParticipantId;
+
+/**
+ * Stub implementation of {@link WaveletListener}.  Each notification method
+ * saves the passed parameters for later inspection by accessors.
+ *
+ * @author [email protected] (David Wang)
+ */
+public class FakeWaveletListener extends WaveletListenerImpl {
+  /** The last participant received from 
+   * {@link #onParticipantAdded(ObservableWavelet, ParticipantId)}
+   */
+  private ParticipantId participant;
+
+  @Override
+  public void onParticipantAdded(ObservableWavelet wavelet, ParticipantId 
participant) {
+    this.participant = participant;
+  }
+
+  /**
+   * @return the last {@code participant} received by 
+   * {@link #onParticipantAdded(ObservableWavelet, ParticipantId)}.
+   */
+  public ParticipantId getParticipant() {
+    return participant;
+  }
+
+
+  /** Resets all fields. */
+  public void reset() {
+    this.participant = null;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/GenericGWTTestBase.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/GenericGWTTestBase.java
 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/GenericGWTTestBase.java
new file mode 100644
index 0000000..98ce42b
--- /dev/null
+++ 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/GenericGWTTestBase.java
@@ -0,0 +1,77 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+import com.google.gwt.junit.client.GWTTestCase;
+
+/**
+ * The base class for running a model-related test case as a GWT test case.
+ *
+ * A {@link GenericGWTTestBase} contains a {@link GenericTestBase}, to which 
it forwards all
+ * relevant testing methods.  This base class holds the reference to the
+ * contained test, and forwards {@link GWTTestCase#gwtSetUp()} and
+ * {@link GWTTestCase#gwtTearDown()} to it.
+ *
+ * To run a vanilla JUnit test case as a GWTTestCase, simply write the JUnit
+ * test as an extension of {@link GenericTestBase}, and create a parallel 
extension
+ * of this class that wraps an instance of the plain test case, and forwards
+ * all test methods to it.
+ *
+ * @param <T> wrapped test case class
+ */
+public abstract class GenericGWTTestBase<T extends GenericTestBase<?>> extends 
GWTTestCase {
+  /** The wrapped vanilla test case. */
+  protected final T target;
+
+  /**
+   * The default constructor.
+   */
+  protected GenericGWTTestBase(T target) {
+    this.target = target;
+  }
+
+  /**
+   * Forwards to wrapped test's {@link GenericTestBase#setUp()}.
+   */
+  @Override
+  protected void gwtSetUp() throws Exception {
+    target.setUp();
+  }
+
+  /**
+   * Forwards to wrapped test's {@link GenericTestBase#tearDown()}.
+   */
+  @Override
+  protected void gwtTearDown() throws Exception {
+    target.tearDown();
+  }
+
+  /**
+   * Specifies a module to use when running this test case. The returned
+   * module must cause the source for this class to be included.
+   *
+   * @see com.google.gwt.junit.client.GWTTestCase#getModuleName()
+   */
+  @Override
+  public String getModuleName() {
+    return "org.waveprotocol.wave.model.tests";
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/GenericTestBase.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/GenericTestBase.java 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/GenericTestBase.java
new file mode 100644
index 0000000..980a987
--- /dev/null
+++ 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/GenericTestBase.java
@@ -0,0 +1,70 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+import junit.framework.TestCase;
+
+/**
+ * Generic base implementation for a test case that tests the behaviour of a
+ * single type.  This implementation holds a reference to a factory for
+ * creating instances of that interface, and uses that factory to instantiates
+ * the instance to test in {@link #setUp()}.
+ *
+ * @param <T> interface type being tested
+ */
+public abstract class GenericTestBase<T> extends TestCase {
+  /** Factory used to create each wave to be tested. */
+  protected final Factory<? extends T> factory;
+
+  // State initialized in setUp()
+
+  /** Target to test. */
+  protected T target;
+
+  /**
+   * Creates this test case, which runs on the wave-datas created by a factory.
+   *
+   * @param factory  factory for creating the wave-datas to test
+   */
+  protected GenericTestBase(Factory<? extends T> factory) {
+    this.factory = factory;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * This implementation uses the test's factory to creates a test target.
+   */
+  @Override
+  protected void setUp() {
+    target = factory.create();
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  protected void tearDown() throws Exception {
+    // This is only overridden to expose tearDown to GWTTestBase (which should
+    // be in GWTTestBase's scope anyway, since it extends TestCase, but for
+    // some reason it isn't).
+    super.tearDown();
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/MockParticipationHelper.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/MockParticipationHelper.java
 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/MockParticipationHelper.java
new file mode 100644
index 0000000..bca7824
--- /dev/null
+++ 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/MockParticipationHelper.java
@@ -0,0 +1,111 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+import org.waveprotocol.wave.model.wave.ParticipantId;
+import org.waveprotocol.wave.model.wave.ParticipationHelper;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+/**
+ * Mock {@link ParticipationHelper}.
+ *
+ */
+public class MockParticipationHelper implements ParticipationHelper {
+
+  /**
+   * Frame used with {@link MockParticipationHelper} to record expectations
+   * and desired results.
+   */
+  public static class Frame {
+    private final Set<ParticipantId> candidates;
+    private final ParticipantId editor;
+    private final ParticipantId result;
+
+    /**
+     * Creates a frame that will either return a given participant or throw an
+     * {@link IllegalStateException} if no participant is given.
+     *
+     * @param result participant to return from this frame, or null if an
+     *        {@link IllegalStateException} should be thrown.
+     * @param editor required for this frame to apply.
+     * @param candidates required for this frame to apply.
+     */
+    public Frame(ParticipantId result, ParticipantId editor,
+        ParticipantId... candidates) {
+      this.result = result;
+      this.editor = editor;
+      this.candidates = new HashSet<ParticipantId>(Arrays.asList(candidates));
+    }
+
+    /** Returns the result or throws the exception dictated by this frame. */
+    public ParticipantId apply() {
+      if (result == null) {
+        throw new IllegalStateException("Authoriser set to throw exception on 
this frame.");
+      } else {
+        return result;
+      }
+    }
+
+    /** Checks whether the given arguments match those expected by this frame. 
*/
+    public boolean matches(ParticipantId editor, Set<ParticipantId> 
candidates) {
+      return editor.equals(this.editor) && candidates.equals(this.candidates);
+    }
+  }
+
+  private final LinkedList<Frame> frames = new LinkedList<Frame>();
+
+  /**
+   * {@inheritDoc}
+   *
+   * Makes a decision by comparing against the next frame in the stub. If
+   * successful, that frame will then be discarded.
+   *
+   * @return the return participant of the frame if the arguments match those 
of
+   *         the frame and the frame includes a return participant.
+   * @throws IllegalStateException if the arguments match those of the frame 
and
+   *         the frame is designed to throw such an exception.
+   * @throws AssertionError if the arguments do not match those of the frame.
+   * @throws NoSuchElementException if there are no frames left.
+   */
+  @Override
+  public ParticipantId getAuthoriser(ParticipantId editor, Set<ParticipantId> 
candidates) {
+    if (frames.isEmpty()) {
+      throw new NoSuchElementException("No frames left to compare with 
getAuthoriser("
+          + editor + ", " + candidates + ")");
+    } else {
+      Frame frame = frames.removeFirst();
+      if (frame.matches(editor, candidates)) {
+        return frame.apply();
+      } else {
+        throw new AssertionError();
+      }
+    }
+  }
+
+  /** Adds a given frame to the end of the list of those expected by this 
stub. */
+  public void program(Frame frame) {
+    frames.addLast(frame);
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/MockWaveletOperationContextFactory.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/MockWaveletOperationContextFactory.java
 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/MockWaveletOperationContextFactory.java
new file mode 100644
index 0000000..df78ca4
--- /dev/null
+++ 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/MockWaveletOperationContextFactory.java
@@ -0,0 +1,61 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+import 
org.waveprotocol.wave.model.operation.wave.AbstractWaveletOperationContextFactory;
+import org.waveprotocol.wave.model.wave.ParticipantId;
+
+/**
+ * WaveletOperationContext.Factory that supports setting the timestamp
+ * and default participant id to use.
+ *
+ */
+public class MockWaveletOperationContextFactory extends 
AbstractWaveletOperationContextFactory {
+  private long timeMillis;
+  private ParticipantId participantId;
+
+  @Override
+  protected long currentTimeMillis() {
+    return timeMillis;
+  }
+
+  @Override
+  public ParticipantId getParticipantId() {
+    return participantId;
+  }
+
+  /**
+   * Sets the timestamp for future WaveletOperationContext objects generated
+   * by this factory.
+   */
+  public MockWaveletOperationContextFactory setCurrentTimeMillis(long 
timeMillis) {
+    this.timeMillis = timeMillis;
+    return this;
+  }
+
+  /**
+   * Sets the participant for future WaveletOperationContext objects generated
+   * by this factory.
+   */
+  public MockWaveletOperationContextFactory setParticipantId(ParticipantId 
participantId) {
+    this.participantId = participantId;
+    return this;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/ModelTestUtils.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/ModelTestUtils.java 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/ModelTestUtils.java
new file mode 100644
index 0000000..8df4800
--- /dev/null
+++ b/wave/src/test/java/org/waveprotocol/wave/model/testing/ModelTestUtils.java
@@ -0,0 +1,61 @@
+/**
+ * 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.
+ */
+
+package org.waveprotocol.wave.model.testing;
+
+import org.waveprotocol.wave.model.document.operation.DocInitialization;
+import org.waveprotocol.wave.model.document.operation.impl.AttributesImpl;
+import 
org.waveprotocol.wave.model.document.operation.impl.DocInitializationBuilder;
+
+/**
+ * A utility class containing convenient methods for creating and checking blip
+ * document content.
+ *
+ */
+public final class ModelTestUtils {
+
+  private ModelTestUtils() {
+  }
+
+  /**
+   * Creates a document with the given content.
+   *
+   * @param contentText The content that the document should have.
+   * @return The document with the given content.
+   */
+  public static DocInitialization createContent(String contentText) {
+    if (contentText.isEmpty()) {
+      return (new DocInitializationBuilder())
+          .elementStart("body", new AttributesImpl())
+          .elementStart("line", new AttributesImpl())
+          .elementEnd()
+          .elementEnd()
+          .build();
+    } else {
+      return new DocInitializationBuilder()
+          .elementStart("body", new AttributesImpl())
+          .elementStart("line", new AttributesImpl())
+          .elementEnd()
+          .characters(contentText)
+          .elementEnd()
+          .build();
+    }
+  }
+
+}

Reply via email to