提交 d1782df6 编写于 作者: J Jesse Wilson

Merge pull request #1343 from square/jw/new-mock

Introduce new mock pattern with a behavior delegate.
......@@ -16,6 +16,5 @@
<modules>
<module>rxjava</module>
<module>rxjava-mock</module>
</modules>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit-adapters</artifactId>
<version>2.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>adapter-rxjava-mock</artifactId>
<name>Adapter: RxJava Mock</name>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>retrofit</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>retrofit-mock</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>adapter-rxjava</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
/*
* Copyright (C) 2015 Square, Inc.
*
* 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 retrofit2.mock;
import rx.Observable;
import rx.functions.Func1;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
public final class RxJavaBehaviorAdapter implements NetworkBehavior.Adapter<Object> {
public static RxJavaBehaviorAdapter create() {
return new RxJavaBehaviorAdapter();
}
private RxJavaBehaviorAdapter() {
}
@Override public Object applyBehavior(NetworkBehavior behavior, Object value) {
if (value instanceof Observable) {
return applyObservableBehavior(behavior, (Observable<?>) value);
}
String name = value.getClass().getCanonicalName();
if ("rx.Single".equals(name)) {
// Apply behavior to the Single from a separate class. This defers classloading such that
// regular Observable operation can be leveraged without relying on this unstable RxJava API.
return SingleHelper.applySingleBehavior(behavior, value);
}
throw new IllegalStateException("Unsupported type " + name);
}
public Observable<?> applyObservableBehavior(final NetworkBehavior behavior,
final Observable<?> value) {
return Observable.timer(behavior.calculateDelay(MILLISECONDS), MILLISECONDS)
.flatMap(new Func1<Long, Observable<?>>() {
@Override public Observable<?> call(Long ignored) {
if (behavior.calculateIsFailure()) {
return Observable.error(behavior.failureException());
}
return value;
}
});
}
}
/*
* Copyright (C) 2015 Square, Inc.
*
* 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 retrofit2.mock;
import rx.Observable;
import rx.Single;
import rx.functions.Func1;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
final class SingleHelper {
@SuppressWarnings("unchecked") // Caller must instanceof / getClass() verify 'value' is Single.
public static Object applySingleBehavior(final NetworkBehavior behavior, Object value) {
final Single<Object> single = (Single<Object>) value;
return Observable.timer(behavior.calculateDelay(MILLISECONDS), MILLISECONDS)
.flatMap(new Func1<Long, Observable<?>>() {
@Override public Observable<?> call(Long ignored) {
if (behavior.calculateIsFailure()) {
return Observable.error(behavior.failureException());
}
return single.toObservable();
}
})
.toSingle();
}
}
/*
* Copyright (C) 2015 Square, Inc.
*
* 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 retrofit2.mock;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Test;
import rx.Observable;
import rx.Single;
import rx.Subscriber;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertTrue;
public final class RxJavaBehaviorAdapterTest {
interface DoWorkService {
Observable<String> observableResponse();
Single<String> singleResponse();
}
private final NetworkBehavior behavior = NetworkBehavior.create(new Random(2847));
private DoWorkService service;
@Before public void setUp() {
DoWorkService mockService = new DoWorkService() {
@Override public Observable<String> observableResponse() {
return Observable.just("Hi!");
}
@Override public Single<String> singleResponse() {
return Single.just("Hi!");
}
};
NetworkBehavior.Adapter<?> adapter = RxJavaBehaviorAdapter.create();
MockRetrofit mockRetrofit = new MockRetrofit(behavior, adapter);
service = mockRetrofit.create(DoWorkService.class, mockService);
}
@Test public void observableFailureAfterDelay() throws InterruptedException {
behavior.setDelay(100, MILLISECONDS);
behavior.setVariancePercent(0);
behavior.setFailurePercent(100);
Observable<String> observable = service.observableResponse();
final long startNanos = System.nanoTime();
final AtomicLong tookMs = new AtomicLong();
final AtomicReference<Throwable> failureRef = new AtomicReference<>();
final CountDownLatch latch = new CountDownLatch(1);
observable.subscribe(new Subscriber<String>() {
@Override public void onNext(String s) {
throw new AssertionError();
}
@Override public void onError(Throwable throwable) {
tookMs.set(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos));
failureRef.set(throwable);
latch.countDown();
}
@Override public void onCompleted() {
}
});
assertTrue(latch.await(1, SECONDS));
assertThat(failureRef.get()).isSameAs(behavior.failureException());
assertThat(tookMs.get()).isGreaterThanOrEqualTo(100);
}
@Test public void observableSuccessAfterDelay() throws InterruptedException {
behavior.setDelay(100, MILLISECONDS);
behavior.setVariancePercent(0);
behavior.setFailurePercent(0);
Observable<String> observable = service.observableResponse();
final long startNanos = System.nanoTime();
final AtomicLong tookMs = new AtomicLong();
final AtomicReference<String> actual = new AtomicReference<>();
final CountDownLatch latch = new CountDownLatch(1);
observable.subscribe(new Subscriber<String>() {
@Override public void onNext(String value) {
tookMs.set(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos));
actual.set(value);
latch.countDown();
}
@Override public void onError(Throwable throwable) {
throw new AssertionError();
}
@Override public void onCompleted() {
}
});
assertTrue(latch.await(1, SECONDS));
assertThat(actual.get()).isEqualTo("Hi!");
assertThat(tookMs.get()).isGreaterThanOrEqualTo(100);
}
@Test public void singleFailureAfterDelay() throws InterruptedException {
behavior.setDelay(100, MILLISECONDS);
behavior.setVariancePercent(0);
behavior.setFailurePercent(100);
Single<String> observable = service.singleResponse();
final long startNanos = System.nanoTime();
final AtomicLong tookMs = new AtomicLong();
final AtomicReference<Throwable> failureRef = new AtomicReference<>();
final CountDownLatch latch = new CountDownLatch(1);
observable.subscribe(new Subscriber<String>() {
@Override public void onNext(String s) {
throw new AssertionError();
}
@Override public void onError(Throwable throwable) {
tookMs.set(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos));
failureRef.set(throwable);
latch.countDown();
}
@Override public void onCompleted() {
}
});
assertTrue(latch.await(1, SECONDS));
assertThat(failureRef.get()).isSameAs(behavior.failureException());
assertThat(tookMs.get()).isGreaterThanOrEqualTo(100);
}
@Test public void singleSuccessAfterDelay() throws InterruptedException {
behavior.setDelay(100, MILLISECONDS);
behavior.setVariancePercent(0);
behavior.setFailurePercent(0);
Single<String> observable = service.singleResponse();
final long startNanos = System.nanoTime();
final AtomicLong tookMs = new AtomicLong();
final AtomicReference<String> actual = new AtomicReference<>();
final CountDownLatch latch = new CountDownLatch(1);
observable.subscribe(new Subscriber<String>() {
@Override public void onNext(String value) {
tookMs.set(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos));
actual.set(value);
latch.countDown();
}
@Override public void onError(Throwable throwable) {
throw new AssertionError();
}
@Override public void onCompleted() {
}
});
assertTrue(latch.await(1, SECONDS));
assertThat(actual.get()).isEqualTo("Hi!");
assertThat(tookMs.get()).isGreaterThanOrEqualTo(100);
}
}
......@@ -18,7 +18,6 @@ package retrofit2.mock;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
......@@ -31,31 +30,21 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
final class BehaviorCall<T> implements Call<T> {
private final NetworkBehavior behavior;
private final ExecutorService backgroundExecutor;
private final Executor callbackExecutor;
private final Call<T> delegate;
private volatile Future<?> task;
private volatile boolean canceled;
private volatile boolean executed;
BehaviorCall(NetworkBehavior behavior, ExecutorService backgroundExecutor,
Executor callbackExecutor, Call<T> delegate) {
if (callbackExecutor == null) {
callbackExecutor = new Executor() {
@Override public void execute(Runnable command) {
command.run();
}
};
}
BehaviorCall(NetworkBehavior behavior, ExecutorService backgroundExecutor, Call<T> delegate) {
this.behavior = behavior;
this.backgroundExecutor = backgroundExecutor;
this.callbackExecutor = callbackExecutor;
this.delegate = delegate;
}
@SuppressWarnings("CloneDoesntCallSuperClone") // We are a final type & this saves clearing state.
@Override public Call<T> clone() {
return new BehaviorCall<>(behavior, backgroundExecutor, callbackExecutor, delegate.clone());
return new BehaviorCall<>(behavior, backgroundExecutor, delegate.clone());
}
@Override public void enqueue(final Callback<T> callback) {
......@@ -70,47 +59,31 @@ final class BehaviorCall<T> implements Call<T> {
try {
Thread.sleep(sleepMs);
} catch (InterruptedException e) {
callFailure(new InterruptedIOException("canceled"));
callback.onFailure(new InterruptedIOException("canceled"));
return false;
}
}
return true;
}
private void callResponse(final Response<T> response) {
callbackExecutor.execute(new Runnable() {
@Override public void run() {
callback.onResponse(response);
}
});
}
private void callFailure(final Throwable throwable) {
callbackExecutor.execute(new Runnable() {
@Override public void run() {
callback.onFailure(throwable);
}
});
}
@Override public void run() {
if (canceled) {
callFailure(new InterruptedIOException("canceled"));
callback.onFailure(new InterruptedIOException("canceled"));
} else if (behavior.calculateIsFailure()) {
if (delaySleep()) {
callFailure(behavior.failureException());
callback.onFailure(behavior.failureException());
}
} else {
delegate.enqueue(new Callback<T>() {
@Override public void onResponse(final Response<T> response) {
if (delaySleep()) {
callResponse(response);
callback.onResponse(response);
}
}
@Override public void onFailure(final Throwable t) {
if (delaySleep()) {
callFailure(t);
callback.onFailure(t);
}
}
});
......
/*
* Copyright (C) 2015 Square, Inc.
*
* 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 retrofit2.mock;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import retrofit2.Call;
import retrofit2.CallAdapter;
import retrofit2.Retrofit;
/**
* Applies {@linkplain NetworkBehavior behavior} to responses and adapts them into the appropriate
* return type using the {@linkplain Retrofit#callAdapterFactories() call adapters} of
* {@link Retrofit}.
*
* @see MockRetrofit#create(Class)
*/
public final class BehaviorDelegate<T> {
private final Retrofit retrofit;
private final NetworkBehavior behavior;
private final Class<T> service;
BehaviorDelegate(Retrofit retrofit, NetworkBehavior behavior, Class<T> service) {
this.retrofit = retrofit;
this.behavior = behavior;
this.service = service;
}
public T returningResponse(Object response) {
return returning(Calls.response(response));
}
@SuppressWarnings("unchecked") // Single-interface proxy creation guarded by parameter safety.
public T returning(Call<?> call) {
final Call<?> behaviorCall =
new BehaviorCall<>(behavior, retrofit.client().getDispatcher().getExecutorService(), call);
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class[] { service },
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Type returnType = method.getGenericReturnType();
Annotation[] methodAnnotations = method.getAnnotations();
CallAdapter<?> callAdapter = retrofit.callAdapter(returnType, methodAnnotations);
return callAdapter.adapt(behaviorCall);
}
});
}
}
/*
* Copyright (C) 2015 Square, Inc.
*
* 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 retrofit2.mock;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import retrofit2.Call;
import retrofit2.Retrofit;
public final class CallBehaviorAdapter implements NetworkBehavior.Adapter<Call<?>> {
private final Executor callbackExecutor;
private final ExecutorService backgroundExecutor;
/**
* Create an instance with a normal {@link Retrofit} instance and an executor service on which
* the simulated delays will be created. Instances of this class should be re-used so that the
* behavior of every mock service is consistent.
*/
public CallBehaviorAdapter(Retrofit retrofit, ExecutorService backgroundExecutor) {
this.callbackExecutor = retrofit.callbackExecutor();
this.backgroundExecutor = backgroundExecutor;
}
@Override public Call<?> applyBehavior(NetworkBehavior behavior, Call<?> value) {
return new BehaviorCall<>(behavior, backgroundExecutor, callbackExecutor, value);
}
}
......@@ -15,34 +15,35 @@
*/
package retrofit2.mock;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import retrofit2.Retrofit;
public final class MockRetrofit {
public static MockRetrofit create(Retrofit retrofit) {
return create(retrofit, NetworkBehavior.create());
}
public static MockRetrofit create(Retrofit retrofit, NetworkBehavior behavior) {
return new MockRetrofit(retrofit, behavior);
}
private final Retrofit retrofit;
private final NetworkBehavior behavior;
private final NetworkBehavior.Adapter<Object> adapter;
@SuppressWarnings("unchecked") //
public MockRetrofit(NetworkBehavior behavior, NetworkBehavior.Adapter<?> adapter) {
this.adapter = (NetworkBehavior.Adapter<Object>) adapter;
public MockRetrofit(Retrofit retrofit, NetworkBehavior behavior) {
this.retrofit = retrofit;
this.behavior = behavior;
}
public Retrofit retrofit() {
return retrofit;
}
public NetworkBehavior networkBehavior() {
return behavior;
}
@SuppressWarnings("unchecked") // Single-interface proxy creation guarded by parameter safety.
public <T> T create(Class<T> service, final T instance) {
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class[] { service },
new InvocationHandler() {
@Override public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
method.setAccessible(true); // Just In Case™
Object value = method.invoke(instance, args);
return adapter.applyBehavior(behavior, value);
}
});
public <T> BehaviorDelegate<T> create(Class<T> service) {
return new BehaviorDelegate<>(retrofit, behavior, service);
}
}
......@@ -30,7 +30,6 @@ import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat;
......@@ -52,20 +51,20 @@ public final class MockRetrofitTest {
.baseUrl("http://example.com")
.build();
DoWorkService mockService = new DoWorkService() {
MockRetrofit mockRetrofit = MockRetrofit.create(retrofit, behavior);
final BehaviorDelegate<DoWorkService> delegate = mockRetrofit.create(DoWorkService.class);
service = new DoWorkService() {
@Override public Call<String> response() {
return Calls.response("Response!");
Call<String> response = Calls.response("Response!");
return delegate.returning(response).response();
}
@Override public Call<String> failure() {
return Calls.failure(mockFailure);
Call<String> failure = Calls.failure(mockFailure);
return delegate.returning(failure).failure();
}
};
NetworkBehavior.Adapter<?> adapter =
new CallBehaviorAdapter(retrofit, newSingleThreadExecutor());
MockRetrofit mockRetrofit = new MockRetrofit(behavior, adapter);
service = mockRetrofit.create(DoWorkService.class, mockService);
}
@Test public void syncFailureThrowsAfterDelay() {
......
......@@ -3,19 +3,16 @@ package com.example.retrofit;
import com.example.retrofit.SimpleService.Contributor;
import com.example.retrofit.SimpleService.GitHub;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import retrofit2.Call;
import retrofit2.Retrofit;
import retrofit2.mock.CallBehaviorAdapter;
import retrofit2.mock.Calls;
import retrofit2.mock.BehaviorDelegate;
import retrofit2.mock.MockRetrofit;
import retrofit2.mock.NetworkBehavior;
......@@ -26,9 +23,11 @@ import retrofit2.mock.NetworkBehavior;
public final class SimpleMockService {
/** A mock implementation of the {@link GitHub} API interface. */
static final class MockGitHub implements GitHub {
private final BehaviorDelegate<GitHub> delegate;
private final Map<String, Map<String, List<Contributor>>> ownerRepoContributors;
public MockGitHub() {
public MockGitHub(BehaviorDelegate<GitHub> delegate) {
this.delegate = delegate;
ownerRepoContributors = new LinkedHashMap<>();
// Seed some mock data.
......@@ -48,7 +47,7 @@ public final class SimpleMockService {
response = contributors;
}
}
return Calls.response(response);
return delegate.returningResponse(response).contributors(owner, repo);
}
public void addContributor(String owner, String repo, String name, int contributions) {
......@@ -72,26 +71,24 @@ public final class SimpleMockService {
.baseUrl(SimpleService.API_URL)
.build();
// Create the Behavior object which manages the fake behavior and the background executor.
// Create a MockRetrofit object with a NetworkBehavior which manages the fake behavior of calls.
NetworkBehavior behavior = NetworkBehavior.create();
ExecutorService bg = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder()
.setNameFormat("mock-retrofit-%d")
.setDaemon(true)
.build());
MockRetrofit mockRetrofit = MockRetrofit.create(retrofit, behavior);
// Create the mock implementation and use MockRetrofit to apply the behavior to it.
MockRetrofit mockRetrofit = new MockRetrofit(behavior, new CallBehaviorAdapter(retrofit, bg));
MockGitHub mockGitHub = new MockGitHub();
GitHub gitHub = mockRetrofit.create(GitHub.class, mockGitHub);
BehaviorDelegate<GitHub> delegate = mockRetrofit.create(GitHub.class);
MockGitHub gitHub = new MockGitHub(delegate);
// Query for some contributors for a few repositories.
printContributors(gitHub, "square", "retrofit");
printContributors(gitHub, "square", "picasso");
// Using the mock object, add some additional mock data.
// Using the mock-only methods, add some additional data.
System.out.println("Adding more mock data...\n");
mockGitHub.addContributor("square", "retrofit", "Foo Bar", 61);
mockGitHub.addContributor("square", "picasso", "Kit Kat", 53);
gitHub.addContributor("square", "retrofit", "Foo Bar", 61);
gitHub.addContributor("square", "picasso", "Kit Kat", 53);
// Reduce the delay to make the next calls complete faster.
behavior.setDelay(500, TimeUnit.MILLISECONDS);
// Query for the contributors again so we can see the mock data that was added.
printContributors(gitHub, "square", "retrofit");
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册