Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,11 @@ public Builder withNotificationCenter(NotificationCenter notificationCenter) {
}

// Helper function for making testing easier
protected Builder withDatafile(String datafile) {
this.datafile = datafile;
return this;
}

protected Builder withBucketing(Bucketer bucketer) {
this.bucketer = bucketer;
return this;
Expand All @@ -1083,11 +1088,6 @@ protected Builder withDecisionService(DecisionService decisionService) {
return this;
}

protected Builder withEventBuilder(EventFactory eventFactory) {
this.eventFactory = eventFactory;
return this;
}

public Optimizely build() {

if (clientEngine == null) {
Expand Down
21 changes: 21 additions & 0 deletions core-api/src/main/java/com/optimizely/ab/event/LogEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.optimizely.ab.event.internal.serializer.Serializer;

import java.util.Map;
import java.util.Objects;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
Expand Down Expand Up @@ -69,6 +70,10 @@ public String getBody() {
return serializer.serialize(eventBatch);
}

public EventBatch getEventBatch() {
return eventBatch;
}

//======== Overriding method ========//

@Override
Expand All @@ -81,6 +86,22 @@ public String toString() {
'}';
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LogEvent logEvent = (LogEvent) o;
return requestMethod == logEvent.requestMethod &&
Objects.equals(endpointUrl, logEvent.endpointUrl) &&
Objects.equals(requestParams, logEvent.requestParams) &&
Objects.equals(eventBatch, logEvent.eventBatch);
}

@Override
public int hashCode() {
return Objects.hash(requestMethod, endpointUrl, requestParams, eventBatch);
}

//======== Helper classes ========//

/**
Expand Down
203 changes: 203 additions & 0 deletions core-api/src/test/java/com/optimizely/ab/EventHandlerRule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/**
* Copyright 2019, Optimizely Inc. and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.optimizely.ab;

import com.optimizely.ab.event.EventHandler;
import com.optimizely.ab.event.LogEvent;
import com.optimizely.ab.event.internal.payload.*;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

import java.util.*;
import java.util.stream.Collectors;

import static com.optimizely.ab.config.ProjectConfig.RESERVED_ATTRIBUTE_PREFIX;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

/**
* EventHandlerRule is a JUnit rule that implements an Optimizely {@link EventHandler}.
*
* This implementation captures events being dispatched in a List.
*
* The List of "actual" events are compared, in order, against a list of "expected" events.
*
* Expected events are validated immediately against the head of actual events. If the queue is empty,
* then a failure is raised. This is to make it easy to map back to the failing test line number.
*
* A failure is raised if at the end of the test there remain non-validated actual events. This is by design
* to ensure that all outbound traffic is known and validated.
*
* TODO this rule does not yet support validation of event tags found in the {@link Event} payload.
*/
public class EventHandlerRule implements EventHandler, TestRule {

private static final String IMPRESSION_EVENT_NAME = "campaign_activated";

private LinkedList<CanonicalEvent> actualEvents;

@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
before();
try {
base.evaluate();
verify();
} finally {
after();
}
}
};
}

private void before() {
actualEvents = new LinkedList<>();
}

private void after() {
}

private void verify() {
assertTrue(actualEvents.isEmpty());
}

public void expectImpression(String experientId, String variationId, String userId) {
expectImpression(experientId, variationId, userId, Collections.emptyMap());
}

public void expectImpression(String experientId, String variationId, String userId, Map<String, ?> attributes) {
verify(experientId, variationId, IMPRESSION_EVENT_NAME, userId, attributes, null);
}

public void expectConversion(String eventName, String userId) {
expectConversion(eventName, userId, Collections.emptyMap());
}

public void expectConversion(String eventName, String userId, Map<String, ?> attributes) {
expectConversion(eventName, userId, attributes, Collections.emptyMap());
}

public void expectConversion(String eventName, String userId, Map<String, ?> attributes, Map<String, ?> tags) {
verify(null, null, eventName, userId, attributes, tags);
}

public void verify(String experientId, String variationId, String eventName, String userId,
Map<String, ?> attributes, Map<String, ?> tags) {
CanonicalEvent expectedEvent = new CanonicalEvent(experientId, variationId, eventName, userId, attributes, tags);
verify(expectedEvent);
}

public void verify(CanonicalEvent expected) {
if (actualEvents.isEmpty()) {
fail(String.format("Expected: %s, but not events are queued", expected));
}

CanonicalEvent actual = actualEvents.removeFirst();
assertEquals(expected, actual);
}

@Override
public void dispatchEvent(LogEvent logEvent) {
List<Visitor> visitors = logEvent.getEventBatch().getVisitors();

if (visitors == null) {
return;
}

for (Visitor visitor: visitors) {
for (Snapshot snapshot: visitor.getSnapshots()) {
List<Decision> decisions = snapshot.getDecisions();
if (decisions == null) {
decisions = new ArrayList<>();
}

if (decisions.isEmpty()) {
decisions.add(new Decision());
}

for (Decision decision: decisions) {
for (Event event: snapshot.getEvents()) {
CanonicalEvent actual = new CanonicalEvent(
decision.getExperimentId(),
decision.getVariationId(),
event.getKey(),
visitor.getVisitorId(),
visitor.getAttributes().stream()
.filter(attribute -> !attribute.getKey().startsWith(RESERVED_ATTRIBUTE_PREFIX))
.collect(Collectors.toMap(Attribute::getKey, Attribute::getValue)),
event.getTags()
);

actualEvents.add(actual);
}
}
}
}
}

private static class CanonicalEvent {
private String experimentId;
private String variationId;
private String eventName;
private String visitorId;
private Map<String, ?> attributes;
private Map<String, ?> tags;

public CanonicalEvent(String experimentId, String variationId, String eventName,
String visitorId, Map<String, ?> attributes, Map<String, ?> tags) {
this.experimentId = experimentId;
this.variationId = variationId;
this.eventName = eventName;
this.visitorId = visitorId;
this.attributes = attributes;
this.tags = tags;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CanonicalEvent that = (CanonicalEvent) o;
return Objects.equals(experimentId, that.experimentId) &&
Objects.equals(variationId, that.variationId) &&
Objects.equals(eventName, that.eventName) &&
Objects.equals(visitorId, that.visitorId) &&
Objects.equals(attributes, that.attributes) &&
Objects.equals(tags, that.tags);
}

@Override
public int hashCode() {
return Objects.hash(experimentId, variationId, eventName, visitorId, attributes, tags);
}

@Override
public String toString() {
return new StringJoiner(", ", CanonicalEvent.class.getSimpleName() + "[", "]")
.add("experimentId='" + experimentId + "'")
.add("variationId='" + variationId + "'")
.add("eventName='" + eventName + "'")
.add("visitorId='" + visitorId + "'")
.add("attributes=" + attributes)
.add("tags=" + tags)
.toString();
}
}
}
Loading