diff --git a/README.md b/README.md index 1e35960..67c8bc3 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,12 @@ Easy Espresso UI testing for Android applications using RxJava. RxPresso makes testing your presentation layer using RxJava as easy as a Unit test. -RxPresso uses [RxMocks](https://github.com/novoda/rxmocks) to generate mocks of your repositories that you can use with RxPresso to control data in your Espresso tests. +RxPresso uses [Mockito](http://mockito.org/) to generate mocks of your repositories that you can use with RxPresso to control data in your Espresso tests. The binding with Espresso Idling resource is handled for you so Espresso will wait until the data you expect to inject in your UI has been delivered to you UI. No more data you don't control in your Espresso test. -At the moment this will only mock methods from the interface returning observables (see Future improvements section). - This project is in its early stages, feel free to comment, and contribute back to help us improve it. ## Adding to your project @@ -26,7 +24,7 @@ buildscript { jcenter() } dependencies { - androidTestCompile 'com.novoda:rxpresso:0.1.5' + androidTestCompile 'com.novoda:rxpresso:0.2.0' } } ``` @@ -34,10 +32,9 @@ buildscript { ## Simple usage -To generate a mocked repo use an interface providing Observables as an abstraction for your repo. -You can now use this interface to generate a mock as shown below. +To generate a mocked repo simply use Mockito. -**Interfaced repository** +**Example repository** ```java public interface DataRepository { @@ -50,7 +47,7 @@ public interface DataRepository { **Mocking this repository** ```java -DataRepository mockedRepo = RxMocks.mock(DataRepository.class) +DataRepository mockedRepo = Mockito.mock(DataRepository.class) ``` You should then replace the repository used by your activities by this mocked one. @@ -62,7 +59,7 @@ Any other option as long as your UI reads from the mocked repo. ```java DataRepository mockedRepo = getSameRepoUsedByUi(); -RxPresso rxpresso = new RxPresso(mockedRepo); +RxPresso rxpresso = RxPresso.init(mockedRepo); Espresso.registerIdlingResources(rxPresso); ``` @@ -131,14 +128,10 @@ DataRepository mockedRepo = getSameRepoUsedByUi(); AnotherDataRepository mockedRepo2 = getSameSecondRepoUsedByUi(); -RxPresso rxpresso = new RxPresso(mockedRepo, mockedRepo2); +RxPresso rxpresso = RxPresso.init(mockedRepo, mockedRepo2); Espresso.registerIdlingResources(rxPresso); ``` -## Future improvements - -- Support "spying" to allow for non mocked calls to be forwarded to actual implementation. - ## Links Here are a list of useful links: diff --git a/build.gradle b/build.gradle index 43067b7..0610880 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:1.2.2' - classpath 'com.novoda:bintray-release:0.2.10' + classpath 'com.novoda:bintray-release:0.3.2' } } diff --git a/core/build.gradle b/core/build.gradle index 8a33f17..fcf5728 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -18,11 +18,12 @@ android { } dependencies { - compile 'com.novoda:rxmocks:0.1.3' - compile 'io.reactivex:rxjava:1.0.10' + compile 'io.reactivex:rxjava:1.0.14' compile 'com.android.support.test.espresso:espresso-core:2.1' + compile 'org.mockito:mockito-core:1.10.19' + compile 'com.google.dexmaker:dexmaker:1.2' + compile 'com.google.dexmaker:dexmaker-mockito:1.2' testCompile 'org.easytesting:fest-assert-core:2.0M10' - testCompile 'org.mockito:mockito-core:1.10.19' } publish { @@ -30,7 +31,7 @@ publish { userOrg = 'novoda' groupId = 'com.novoda' artifactId = 'rxpresso' - publishVersion = '0.1.5' + publishVersion = '0.2.0' description = 'Easy espresso testing for projects using RxJava' website = 'https://github.com/novoda/rxpresso' } diff --git a/core/src/main/java/com/novoda/rxpresso/Expect.java b/core/src/main/java/com/novoda/rxpresso/Expect.java index 26eec47..ebc9468 100644 --- a/core/src/main/java/com/novoda/rxpresso/Expect.java +++ b/core/src/main/java/com/novoda/rxpresso/Expect.java @@ -2,9 +2,9 @@ import android.support.test.espresso.IdlingResource; -import com.novoda.rxmocks.RxExpect; -import com.novoda.rxmocks.RxMatcher; -import com.novoda.rxmocks.RxMocks; +import com.novoda.rxpresso.matcher.RxExpect; +import com.novoda.rxpresso.matcher.RxMatcher; +import com.novoda.rxpresso.mock.RxMock; import java.util.concurrent.atomic.AtomicBoolean; @@ -16,16 +16,16 @@ public class Expect implements IdlingResource { - private final Object repo; private final Observable observable; + private final RxMock mock; private final Observable source; private final AtomicBoolean idle = new AtomicBoolean(true); private Subscription subscription; private ResourceCallback resourceCallback; - Expect(Object repo, Observable source, Observable observable) { - this.repo = repo; + Expect(RxMock mock, Observable source, Observable observable) { + this.mock = mock; this.source = source; this.observable = observable; } @@ -40,9 +40,7 @@ public class Expect implements IdlingResource { */ public Then expect(RxMatcher> matcher) { expectAnyMatching(matcher); - RxMocks.with(repo) - .sendEventsFrom(source) - .to(observable); + mock.sendEventsFrom(source).to(observable); return new Then(); } @@ -55,44 +53,44 @@ public Then expect(RxMatcher> matcher) { */ public Then expectOnly(RxMatcher> matcher) { expectOnlyMatching(matcher); - RxMocks.with(repo) - .sendEventsFrom(source) - .to(observable); + mock.sendEventsFrom(source).to(observable); return new Then(); } private void expectAnyMatching(RxMatcher> matcher) { RxErrorRethrower.register(); idle.compareAndSet(true, false); - subscription = RxMocks.with(repo) - .getEventsFor(observable) - .subscribe( - RxExpect.expect( - matcher, new Action1>() { - @Override - public void call(Notification notification) { - subscription.unsubscribe(); - RxErrorRethrower.unregister(); - transitionToIdle(); - } - })); + + subscription = mock.getEventsFor(observable).subscribe( + RxExpect.expect( + matcher, new Action1>() { + @Override + public void call(Notification tNotification) { + subscription.unsubscribe(); + RxErrorRethrower.unregister(); + transitionToIdle(); + } + } + ) + ); } private void expectOnlyMatching(RxMatcher> matcher) { RxErrorRethrower.register(); idle.compareAndSet(true, false); - subscription = RxMocks.with(repo) - .getEventsFor(observable) - .subscribe( - RxExpect.expectOnly( - matcher, new Action1>() { - @Override - public void call(Notification notification) { - subscription.unsubscribe(); - RxErrorRethrower.unregister(); - transitionToIdle(); - } - })); + + subscription = mock.getEventsFor(observable).subscribe( + RxExpect.expectOnly( + matcher, new Action1>() { + @Override + public void call(Notification tNotification) { + subscription.unsubscribe(); + RxErrorRethrower.unregister(); + transitionToIdle(); + } + } + ) + ); } private void transitionToIdle() { diff --git a/core/src/main/java/com/novoda/rxpresso/RxPresso.java b/core/src/main/java/com/novoda/rxpresso/RxPresso.java index 6be142c..8bd6605 100644 --- a/core/src/main/java/com/novoda/rxpresso/RxPresso.java +++ b/core/src/main/java/com/novoda/rxpresso/RxPresso.java @@ -2,7 +2,7 @@ import android.support.test.espresso.IdlingResource; -import com.novoda.rxmocks.RxMocks; +import com.novoda.rxpresso.mock.RxMock; import java.util.ArrayList; import java.util.Collections; @@ -11,43 +11,54 @@ import rx.Observable; import rx.functions.Func1; -public class RxPresso implements IdlingResource { +public final class RxPresso implements IdlingResource { - private final Object[] repositories; + private final List mocks; private final List pendingResources = Collections.synchronizedList(new ArrayList()); private ResourceCallback resourceCallback; /** - * @param repositories The different mocked repositories you want to control in your tests + * @param mocks The different mocked repositories you want to control in your tests */ - public RxPresso(Object... repositories) { - this.repositories = repositories; + public static RxPresso from(Object... mocks) { + return new RxPresso(Observable.from(mocks).map(asRxMocks).toList().toBlocking().first()); } /** - /** - * Initiate an action on the given mocked {@code observable} - * @param observable The mocked observable to work with - * @param The type of the observable - * @return The With object to interact with the given mocked {@code observable} + * @param mocks The different mocked repositories you want to control in your tests */ + private RxPresso(List mocks) { + this.mocks = mocks; + } + public With given(Observable observable) { - Object repo = Observable.from(repositories).filter(provides(observable)).toBlocking().first(); - final With with = new With<>(repo, observable); + RxMock mock = Observable.from(mocks).filter(provides(observable)).toBlocking().first(); + final With with = new With<>(mock, observable); pendingResources.add(with); - with.registerIdleTransitionCallback(new ResourceCallback() { - @Override - public void onTransitionToIdle() { - pendingResources.remove(with); - if (pendingResources.isEmpty()) { - resourceCallback.onTransitionToIdle(); + with.registerIdleTransitionCallback( + new ResourceCallback() { + @Override + public void onTransitionToIdle() { + pendingResources.remove(with); + if (pendingResources.isEmpty()) { + resourceCallback.onTransitionToIdle(); + } + } } - } - }); + ); return with; } + private static Func1 provides(final Observable observable) { + return new Func1() { + @Override + public Boolean call(RxMock rxMock) { + return rxMock.provides(observable); + } + }; + } + @Override public String getName() { return "RxPresso"; @@ -66,29 +77,23 @@ public void registerIdleTransitionCallback(ResourceCallback resourceCallback) { this.resourceCallback = resourceCallback; } - /** - * Resets all the mocked observables from the repositories registered by RxPresso - */ public void resetMocks() { - for (Object repo : repositories) { - RxMocks.with(repo).resetMocks(); + for (RxMock mock : mocks) { + mock.resetMocks(); } } - private static Func1 provides(final Observable observable) { - return new Func1() { - @Override - public Boolean call(Object repo) { - return RxMocks.with(repo).provides(observable); - } - }; - } - - private static Func1 isIdle = new Func1() { + private static final Func1 isIdle = new Func1() { @Override public Boolean call(IdlingResource resource) { return resource.isIdleNow(); } }; + private static final Func1 asRxMocks = new Func1() { + @Override + public RxMock call(Object object) { + return RxMock.from(object); + } + }; } diff --git a/core/src/main/java/com/novoda/rxpresso/With.java b/core/src/main/java/com/novoda/rxpresso/With.java index 2c21432..535e106 100644 --- a/core/src/main/java/com/novoda/rxpresso/With.java +++ b/core/src/main/java/com/novoda/rxpresso/With.java @@ -2,33 +2,38 @@ import android.support.test.espresso.IdlingResource; +import com.novoda.rxpresso.mock.RxMock; + import rx.Observable; public class With implements IdlingResource { - private final Object repo; + private final RxMock mock; private final Observable observable; private ResourceCallback resourceCallback; private Expect expect; - With(Object repo, Observable observable) { - this.repo = repo; + With(RxMock mock, Observable observable) { + this.mock = mock; this.observable = observable; } /** * Setup the injection of the events from the {@code source} into the mocked {@code observable} + * * @param source An observable providing the events to inject * @return An Expect object to trigger the injection and setup what event to expect and wait for. */ public Expect withEventsFrom(Observable source) { - expect = new Expect<>(repo, source, observable); - expect.registerIdleTransitionCallback(new ResourceCallback() { - @Override - public void onTransitionToIdle() { - resourceCallback.onTransitionToIdle(); - } - }); + expect = new Expect<>(mock, source, observable); + expect.registerIdleTransitionCallback( + new ResourceCallback() { + @Override + public void onTransitionToIdle() { + resourceCallback.onTransitionToIdle(); + } + } + ); return expect; } diff --git a/core/src/main/java/com/novoda/rxpresso/matcher/RxExpect.java b/core/src/main/java/com/novoda/rxpresso/matcher/RxExpect.java new file mode 100644 index 0000000..427d925 --- /dev/null +++ b/core/src/main/java/com/novoda/rxpresso/matcher/RxExpect.java @@ -0,0 +1,197 @@ +package com.novoda.rxpresso.matcher; + +import rx.Notification; +import rx.Observable; +import rx.functions.Action1; + +public final class RxExpect { + + private RxExpect() { + } + + /** + * Asserts that a given {@code observable} emits an element matching a given {@code matcher} + * + * @param matcher The matcher to use for the assertion + * @param observable The observable to assert against + * @param The type of the observable + */ + public static void expect(RxMatcher> matcher, Observable observable) { + observable.materialize() + .subscribe(expect(matcher)); + } + + /** + * Asserts that a given {@code observable} emits only elements matching a given {@code matcher} + * + * @param matcher The matcher to use for the assertion + * @param observable The observable to assert against + * @param The type of the observable + */ + public static void expectOnly(RxMatcher> matcher, Observable observable) { + observable.materialize() + .subscribe(expectOnly(matcher)); + } + + /** + * Asserts that a given {@code observable} emits an element matching a given {@code matcher} * + * + * @param matcher The matcher to use for the assertion + * @param observable The observable to assert against + * @param matched A callback for when the assertion is matched + * @param The type of the observable + */ + public static void expect(final RxMatcher> matcher, final Observable observable, final Action1> matched) { + observable.materialize() + .subscribe(expect(matcher, matched)); + } + + /** + * Asserts that a given {@code observable} emits only elements matching a given {@code matcher} * + * + * @param matcher The matcher to use for the assertion + * @param observable The observable to assert against + * @param matched A callback for when the assertion is matched + * @param The type of the observable + */ + public static void expectOnly(final RxMatcher> matcher, final Observable observable, final Action1> matched) { + observable.materialize() + .subscribe(expectOnly(matcher, matched)); + } + + /** + * Returns an action to subscribe to an observable to assert if it emits an element matching a given {@code matcher} + * + * @param matcher The matcher to use for the assertion + * @param The type of the observable + * @return The action to subscribe to a materialized observable to assert if a given event is emitted. + */ + public static Action1> expect(final RxMatcher> matcher) { + return expect(matcher, doNothing); + } + + /** + * Returns an action to subscribe to an observable to assert if it emits only elements matching a given {@code matcher} + * + * @param matcher The matcher to use for the assertion + * @param The type of the observable + * @return The action to subscribe to a materialized observable to assert if a given event is emitted. + */ + public static Action1> expectOnly(final RxMatcher> matcher) { + return expectOnly(matcher, doNothing); + } + + /** + * Returns an action to subscribe to an observable to assert if it emits only elements matching a given {@code matcher} * + * + * @param matcher The matcher to use for the assertion + * @param matched A callback for when the assertion is matched + * @param The type of the observable + * @return The action to subscribe to a materialized observable to assert if a given event is emitted. + */ + public static Action1> expectOnly(final RxMatcher> matcher, final Action1> matched) { + return new Action1>() { + @Override + public void call(Notification notification) { + if (matcher.matches(notification)) { + matched.call(notification); + } else { + throw new RuntimeException("Expected " + matcher.description() + " but got " + notification); + } + } + }; + } + + /** + * Returns an action to subscribe to an observable to assert if it emits an element matching a given {@code matcher} * + * + * @param matcher The matcher to use for the assertion + * @param matched A callback for when the assertion is matched + * @param The type of the observable + * @return The action to subscribe to a materialized observable to assert if a given event is emitted. + */ + public static Action1> expect(final RxMatcher> matcher, final Action1> matched) { + return new Action1>() { + + private boolean noMatch = true; + + @Override + public void call(Notification notification) { + if (matcher.matches(notification)) { + noMatch = false; + matched.call(notification); + } + if (notification.getKind() == Notification.Kind.OnCompleted && noMatch) { + throw new RuntimeException("Expected " + matcher.description() + " but completed without matching"); + } + } + }; + } + + /** + * @param clazz The class of the type {@code T} to match + * @param The type to match + * @return a matcher matching any onNext event of a given type {@code T} + */ + public static RxMatcher> any(Class clazz) { + return new RxMatcher>() { + @Override + public boolean matches(Notification actual) { + return actual.getKind() == Notification.Kind.OnNext; + } + + @Override + public String description() { + return "Notification with kind " + Notification.Kind.OnNext; + } + }; + } + + /** + * @param clazz The class of the type {@code T} of the observable to assert against + * @param The type of the observable to assert against + * @return a matcher matching any onError event + */ + public static RxMatcher> anyError(Class clazz) { + return new RxMatcher>() { + @Override + public boolean matches(Notification actual) { + return actual.getKind() == Notification.Kind.OnError; + } + + @Override + public String description() { + return "Notification with kind " + Notification.Kind.OnError; + } + }; + } + + /** + * @param clazz The class of the type {@code T} of the observable to assert against + * @param errorClazz The class of the type {@code V} of the error to match + * @param The type of the observable to assert against + * @param + * @return a matcher matching any onError event with an error a given type {@code V} + */ + public static RxMatcher> anyError(Class clazz, final Class errorClazz) { + return new RxMatcher>() { + @Override + public boolean matches(Notification actual) { + return actual.hasThrowable() && actual.getThrowable().getClass().isAssignableFrom(errorClazz); + } + + @Override + public String description() { + return "Notification with error of type " + errorClazz.getName(); + } + }; + } + + private static final Action1 doNothing = new Action1() { + @Override + public void call(Object o) { + //Do Nothing + } + }; + +} diff --git a/core/src/main/java/com/novoda/rxpresso/matcher/RxMatcher.java b/core/src/main/java/com/novoda/rxpresso/matcher/RxMatcher.java new file mode 100644 index 0000000..9a11532 --- /dev/null +++ b/core/src/main/java/com/novoda/rxpresso/matcher/RxMatcher.java @@ -0,0 +1,9 @@ +package com.novoda.rxpresso.matcher; + +public interface RxMatcher { + + boolean matches(T actual); + + String description(); + +} diff --git a/core/src/main/java/com/novoda/rxpresso/mock/Functions.java b/core/src/main/java/com/novoda/rxpresso/mock/Functions.java new file mode 100644 index 0000000..63cd64c --- /dev/null +++ b/core/src/main/java/com/novoda/rxpresso/mock/Functions.java @@ -0,0 +1,11 @@ +package com.novoda.rxpresso.mock; + +import rx.Observable; + +final class Functions { + + static Observable.Operator infinite() { + return new InfiniteOperator<>(); + } + +} diff --git a/core/src/main/java/com/novoda/rxpresso/mock/InfiniteOperator.java b/core/src/main/java/com/novoda/rxpresso/mock/InfiniteOperator.java new file mode 100644 index 0000000..35d6984 --- /dev/null +++ b/core/src/main/java/com/novoda/rxpresso/mock/InfiniteOperator.java @@ -0,0 +1,26 @@ +package com.novoda.rxpresso.mock; + +import rx.Observable; +import rx.Subscriber; + +class InfiniteOperator implements Observable.Operator { + @Override + public Subscriber call(final Subscriber subscriber) { + return new Subscriber() { + @Override + public void onCompleted() { + //Swallow + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onNext(T t) { + subscriber.onNext(t); + } + }; + } +} diff --git a/core/src/main/java/com/novoda/rxpresso/mock/Pair.java b/core/src/main/java/com/novoda/rxpresso/mock/Pair.java new file mode 100644 index 0000000..fb2ffe7 --- /dev/null +++ b/core/src/main/java/com/novoda/rxpresso/mock/Pair.java @@ -0,0 +1,66 @@ +package com.novoda.rxpresso.mock; + +class Pair { + public final F first; + public final S second; + + /** + * Constructor for a Pair. + * + * @param first the first object in the Pair + * @param second the second object in the pair + */ + public Pair(F first, S second) { + this.first = first; + this.second = second; + } + + /** + * Checks the two objects for equality by delegating to their respective + * {@link Object#equals(Object)} methods. + * + * @param o the {@link Pair} to which this one is to be checked for equality + * @return true if the underlying objects of the Pair are both considered + * equal + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Pair pair = (Pair) o; + + if (first != null ? !first.equals(pair.first) : pair.first != null) { + return false; + } + if (second != null ? !second.equals(pair.second) : pair.second != null) { + return false; + } + + return true; + } + + /** + * Compute a hash code using the hash codes of the underlying objects + * + * @return a hashcode of the Pair + */ + @Override + public int hashCode() { + return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode()); + } + + /** + * Convenience method for creating an appropriately typed pair. + * @param a the first object in the Pair + * @param b the second object in the pair + * @return a Pair that is templatized with the types of a and b + */ + public static Pair create(A a, B b) { + return new Pair(a, b); + } +} diff --git a/core/src/main/java/com/novoda/rxpresso/mock/RxMock.java b/core/src/main/java/com/novoda/rxpresso/mock/RxMock.java new file mode 100644 index 0000000..246fc18 --- /dev/null +++ b/core/src/main/java/com/novoda/rxpresso/mock/RxMock.java @@ -0,0 +1,244 @@ +package com.novoda.rxpresso.mock; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import rx.Notification; +import rx.Observable; +import rx.Subscriber; +import rx.Subscription; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Func2; +import rx.subjects.ClearableBehaviorSubject; +import rx.subjects.PublishSubject; +import rx.subscriptions.BooleanSubscription; + +import static com.novoda.rxpresso.mock.Functions.infinite; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; + +public final class RxMock { + + private final Object mock; + private final Map observableHashMap = new HashMap<>(); + private final Map, PublishSubject>> mapSubject = new HashMap<>(); + + public static RxMock mock(Class clazz) { + return from(Mockito.mock(clazz)); + } + + public static RxMock from(Object mock) { + RxMock rxMock = new RxMock(mock); + rxMock.setMockResponses(); + return rxMock; + } + + private RxMock(Object mock) { + this.mock = mock; + } + + private void setMockResponses() { + for (Method method : mock.getClass().getMethods()) { + if (method.getReturnType().equals(Observable.class) && isMockable(method)) { + setupMockResponseFor(method); + } + } + } + + private boolean isMockable(Method method) { + return !Modifier.isPrivate(method.getModifiers()) + && !Modifier.isProtected(method.getModifiers()) + && !Modifier.isStatic(method.getModifiers()); + } + + public Boolean provides(Observable observable) { + return observableHashMap.containsValue(observable); + } + + public Observable> getEventsFor(Observable observable) { + Pair, PublishSubject> subjectPair = mapSubject.get(observable); + if (subjectPair == null) { + throw new IllegalArgumentException( + "The observable " + observable + + " is not provided by this repo use the provides(Observable o) method to check first"); + } + return Observable.zip(subjectPair.first, subjectPair.second, unzip()) + .lift(clearOnUnsubscribe(observable)); + } + + /** + * Inject the events from given {@code source} into a mocked pipeline + * + * @param source The observable producing the events to inject + * @param The type of this observable + * @return A sender object to define into which pipeline to inject the events. + */ + public RxObservableSender sendEventsFrom(Observable source) { + return new RxObservableSender<>(source); + } + + public class RxObservableSender { + + private final Observable source; + + public RxObservableSender(Observable source) { + this.source = source; + } + + /** + * Send the events from {@code source} to the given mocked {@code observable} + * + * @param observable The mocked observable to inject events into. + */ + public void to(Observable observable) { + ((Observable) source).materialize().lift(infinite()).subscribe(mapSubject.get(observable).first); + } + + } + + public void resetMocks() { + observableHashMap.clear(); + mapSubject.clear(); + } + + private void setupMockResponseFor(Method method) { + when(invoke(method)).thenAnswer( + new Answer() { + @Override + public Observable answer(InvocationOnMock invocation) throws Throwable { + String key = getKeyFor(invocation.getMethod(), invocation.getArguments()); + if (!observableHashMap.containsKey(key)) { + initialiseMockedObservable(invocation.getMethod(), invocation.getArguments()); + } + return observableHashMap.get(key); + } + } + ); + } + + private Object invoke(Method method) { + try { + return method.invoke(mock, getArgumentsFor(method)); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + return null; + } + + private Object[] getArgumentsFor(Method method) { + List arguments = new ArrayList<>(); + for (Class aClass : method.getParameterTypes()) { + arguments.add(any(aClass)); + } + return arguments.toArray(); + } + + private void initialiseMockedObservable(Method method, Object[] args) { + ClearableBehaviorSubject subject = ClearableBehaviorSubject.create(); + PublishSubject notificationSubject = PublishSubject.create(); + final String keyForArgs = getKeyFor(method, args); + final Observable observable = subject + .dematerialize() + .doOnEach(new NotifyDataEvent(notificationSubject)) + .lift(new SwallowUnsubscribe()); + observableHashMap.put(keyForArgs, observable); + mapSubject.put(observable, new Pair<>(subject, notificationSubject)); + } + + private String getKeyFor(Method method, Object[] args) { + StringBuilder keyBuilder = new StringBuilder(method.getName()); + int index = 0; + for (Class type : method.getParameterTypes()) { + keyBuilder.append('#').append(type.getSimpleName()).append('-').append(args[index++].hashCode()); + } + return keyBuilder.toString(); + } + + private AddUnsubscribe clearOnUnsubscribe(final Object observable) { + return new AddUnsubscribe( + BooleanSubscription.create( + new Action0() { + @Override + public void call() { + mapSubject.get(observable).first.clear(); + } + } + ) + ); + } + + private static class NotifyDataEvent implements Action1> { + + private final PublishSubject> publishSubject; + + public NotifyDataEvent(PublishSubject> publishSubject) { + this.publishSubject = publishSubject; + } + + @Override + public void call(Notification notification) { + publishSubject.onNext((Notification) notification); + } + } + + private static class SwallowUnsubscribe implements Observable.Operator { + + @Override + public Subscriber call(final Subscriber subscriber) { + return new Subscriber() { + @Override + public void onCompleted() { + subscriber.onCompleted(); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onNext(T t) { + subscriber.onNext(t); + } + }; + } + + } + + private static class AddUnsubscribe implements Observable.Operator { + + private final Subscription unsubscribe; + + private AddUnsubscribe(Subscription unsubscribe) { + this.unsubscribe = unsubscribe; + } + + @Override + public Subscriber call(final Subscriber subscriber) { + subscriber.add(unsubscribe); + return subscriber; + } + + } + + private static Func2 unzip() { + return new Func2() { + @Override + public Notification call(Notification first, Notification second) { + return second; + } + }; + } +} diff --git a/core/src/main/java/com/novoda/rxpresso/mock/SingleEvent.java b/core/src/main/java/com/novoda/rxpresso/mock/SingleEvent.java new file mode 100644 index 0000000..624a74f --- /dev/null +++ b/core/src/main/java/com/novoda/rxpresso/mock/SingleEvent.java @@ -0,0 +1,36 @@ +package com.novoda.rxpresso.mock; + +import rx.Observable; + +public final class SingleEvent { + + private SingleEvent() { + } + + /** + * @param value The value to emit + * @param The type of the observable + * @return An observable emitting only one onNext event and never completing. (Useful to inject single events in the mocked observables) + */ + public static Observable onNext(T value) { + return Observable.just(value).lift(Functions.infinite()); + } + + /** + * @param error The error to emit + * @param The type of the observable + * @return An observable emitting only one onError event + */ + public static Observable onError(Throwable error) { + return Observable.error(error); + } + + /** + * @param The type of the observable + * @return An observable emitting only one onCompleted event + */ + public static Observable onCompleted() { + return Observable.empty(); + } + +} diff --git a/core/src/main/java/rx/subjects/ClearableBehaviorSubject.java b/core/src/main/java/rx/subjects/ClearableBehaviorSubject.java new file mode 100644 index 0000000..b5b9c9d --- /dev/null +++ b/core/src/main/java/rx/subjects/ClearableBehaviorSubject.java @@ -0,0 +1,234 @@ +package rx.subjects; /** + * Copyright 2014 Netflix, 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. + */ + +import java.util.ArrayList; +import java.util.List; + +import rx.annotations.Experimental; +import rx.exceptions.Exceptions; +import rx.functions.Action1; +import rx.internal.operators.NotificationLite; +import rx.subjects.SubjectSubscriptionManager.SubjectObserver; + +/** + * Subject that emits the most recent item it has observed and all subsequent observed items to each subscribed + * {@link rx.Observer}. + *

+ * + *

+ * Example usage: + *

+ *

 {@code
+
+  // observer will receive all events.
+  BehaviorSubject subject = BehaviorSubject.create("default");
+  subject.subscribe(observer);
+  subject.onNext("one");
+  subject.onNext("two");
+  subject.onNext("three");
+
+  // observer will receive the "one", "two" and "three" events, but not "zero"
+  BehaviorSubject subject = BehaviorSubject.create("default");
+  subject.onNext("zero");
+  subject.onNext("one");
+  subject.subscribe(observer);
+  subject.onNext("two");
+  subject.onNext("three");
+
+  // observer will receive only onCompleted
+  BehaviorSubject subject = BehaviorSubject.create("default");
+  subject.onNext("zero");
+  subject.onNext("one");
+  subject.onCompleted();
+  subject.subscribe(observer);
+
+  // observer will receive only onError
+  BehaviorSubject subject = BehaviorSubject.create("default");
+  subject.onNext("zero");
+  subject.onNext("one");
+  subject.onError(new RuntimeException("error"));
+  subject.subscribe(observer);
+  } 
+ *
+ * @param 
+ *          the type of item expected to be observed by the Subject
+ */
+public final class ClearableBehaviorSubject extends Subject {
+    /**
+     * Creates a {@link ClearableBehaviorSubject} without a default item.
+     *
+     * @param 
+     *            the type of item the Subject will emit
+     * @return the constructed {@link ClearableBehaviorSubject}
+     */
+    public static  ClearableBehaviorSubject create() {
+        return create(null, false);
+    }
+    /**
+     * Creates a {@link ClearableBehaviorSubject} that emits the last item it observed and all subsequent items to each
+     * {@link rx.Observer} that subscribes to it.
+     *
+     * @param 
+     *            the type of item the Subject will emit
+     * @param defaultValue
+     *            the item that will be emitted first to any {@link rx.Observer} as long as the
+     *            {@link ClearableBehaviorSubject} has not yet observed any items from its source {@code Observable}
+     * @return the constructed {@link ClearableBehaviorSubject}
+     */
+    public static  ClearableBehaviorSubject create(T defaultValue) {
+        return create(defaultValue, true);
+    }
+    private static  ClearableBehaviorSubject create(T defaultValue, boolean hasDefault) {
+        final SubjectSubscriptionManager state = new SubjectSubscriptionManager();
+        if (hasDefault) {
+            state.set(NotificationLite.instance().next(defaultValue));
+        }
+        state.onAdded = new Action1>() {
+
+            @Override
+            public void call(SubjectObserver o) {
+                o.emitFirst(state.get(), state.nl);
+            }
+            
+        };
+        state.onTerminated = state.onAdded;
+        return new ClearableBehaviorSubject(state, state);
+    }
+
+    private final SubjectSubscriptionManager state;
+    private final NotificationLite nl = NotificationLite.instance();
+
+    protected ClearableBehaviorSubject(OnSubscribe onSubscribe, SubjectSubscriptionManager state) {
+        super(onSubscribe);
+        this.state = state;
+    }
+
+    public void clear() {
+        state.set(null);
+    }
+
+    @Override
+    public void onCompleted() {
+        Object last = state.get();
+        if (last == null || state.active) {
+            Object n = nl.completed();
+            for (SubjectObserver bo : state.terminate(n)) {
+                bo.emitNext(n, state.nl);
+            }
+        }
+    }
+
+    @Override
+    public void onError(Throwable e) {
+        Object last = state.get();
+        if (last == null || state.active) {
+            Object n = nl.error(e);
+            List errors = null;
+            for (SubjectObserver bo : state.terminate(n)) {
+                try {
+                    bo.emitNext(n, state.nl);
+                } catch (Throwable e2) {
+                    if (errors == null) {
+                        errors = new ArrayList();
+                    }
+                    errors.add(e2);
+                }
+            }
+
+            Exceptions.throwIfAny(errors);
+        }
+    }
+
+    @Override
+    public void onNext(T v) {
+        Object last = state.get();
+        if (last == null || state.active) {
+            Object n = nl.next(v);
+            for (SubjectObserver bo : state.next(n)) {
+                bo.emitNext(n, state.nl);
+            }
+        }
+    }
+
+    /* test support */ int subscriberCount() {
+        return state.observers().length;
+    }
+
+    @Override
+    public boolean hasObservers() {
+        return state.observers().length > 0;
+    }
+    /**
+     * Check if the Subject has a value.
+     * 

Use the {@link #getValue()} method to retrieve such a value. + *

Note that unless {@link #hasCompleted()} or {@link #hasThrowable()} returns true, the value + * retrieved by {@code getValue()} may get outdated. + * @return true if and only if the subject has some value and hasn't terminated yet. + */ + @Experimental + public boolean hasValue() { + Object o = state.get(); + return nl.isNext(o); + } + /** + * Check if the Subject has terminated with an exception. + * @return true if the subject has received a throwable through {@code onError}. + */ + @Experimental + public boolean hasThrowable() { + Object o = state.get(); + return nl.isError(o); + } + /** + * Check if the Subject has terminated normally. + * @return true if the subject completed normally via {@code onCompleted()} + */ + @Experimental + public boolean hasCompleted() { + Object o = state.get(); + return nl.isCompleted(o); + } + /** + * Returns the current value of the Subject if there is such a value and + * the subject hasn't terminated yet. + *

The can return {@code null} for various reasons. Use {@link #hasValue()}, {@link #hasThrowable()} + * and {@link #hasCompleted()} to determine if such {@code null} is a valid value, there was an + * exception or the Subject terminated (with or without receiving any value). + * @return the current value or {@code null} if the Subject doesn't have a value, + * has terminated or has an actual {@code null} as a valid value. + */ + @Experimental + public T getValue() { + Object o = state.get(); + if (nl.isNext(o)) { + return nl.getValue(o); + } + return null; + } + /** + * Returns the Throwable that terminated the Subject. + * @return the Throwable that terminated the Subject or {@code null} if the + * subject hasn't terminated yet or it terminated normally. + */ + @Experimental + public Throwable getThrowable() { + Object o = state.get(); + if (nl.isError(o)) { + return nl.getError(o); + } + return null; + } +} diff --git a/core/src/test/java/com/novoda/rxpresso/RxExpectTest.java b/core/src/test/java/com/novoda/rxpresso/RxExpectTest.java new file mode 100644 index 0000000..557d764 --- /dev/null +++ b/core/src/test/java/com/novoda/rxpresso/RxExpectTest.java @@ -0,0 +1,52 @@ +package com.novoda.rxpresso; + +import com.novoda.rxpresso.mock.SingleEvent; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import rx.Observable; + +import static com.novoda.rxpresso.matcher.RxExpect.*; + +public class RxExpectTest { + + @Rule + ExpectedException expectedException = ExpectedException.none(); + + @Test + public void expectMatchesAccordingToMatcher() throws Exception { + Observable observableToTest = Observable.just(42); + + observableToTest.materialize() + .subscribe(expect(any(Integer.class))); + } + + @Test + public void expectFailsIfNoEventMatchesMatcher() throws Exception { + expectedException.expectMessage("Expected Notification with kind OnError but completed without matching"); + + Observable observableToTest = Observable.just(42); + + observableToTest.materialize() + .subscribe(expect(anyError(Integer.class))); + } + + @Test + public void expectOnlyMatchesAccordingToMatcher() throws Exception { + Observable observableToTest = SingleEvent.onNext(42); + + observableToTest.materialize() + .subscribe(expectOnly(any(Integer.class))); + } + + @Test + public void expectOnlyFailsIfAnEventDoesNotMatchMatcher() throws Exception { + expectedException.expectMessage("Expected Notification with kind OnNext but got"); + Observable observableToTest = Observable.just(42); + + observableToTest.materialize() + .subscribe(expectOnly(any(Integer.class))); + } +} diff --git a/core/src/test/java/com/novoda/rxpresso/RxMocksTest.java b/core/src/test/java/com/novoda/rxpresso/RxMocksTest.java new file mode 100644 index 0000000..9177079 --- /dev/null +++ b/core/src/test/java/com/novoda/rxpresso/RxMocksTest.java @@ -0,0 +1,124 @@ +package com.novoda.rxpresso; + +import com.novoda.rxpresso.mock.RxMock; +import com.novoda.rxpresso.mock.SingleEvent; + +import java.lang.reflect.Array; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import rx.Notification; +import rx.Observable; +import rx.functions.Action1; + +import static org.fest.assertions.api.Assertions.assertThat; + +public class RxMocksTest { + + private TestRepository mockedRepo; + private RxMock rxMock; + + @Before + public void setUp() throws Exception { + mockedRepo = Mockito.mock(TestRepository.class); + rxMock = RxMock.from(mockedRepo); + } + + @Test + public void itSendsEventsToMockedObservable() throws Exception { + Observable foo = mockedRepo.foo(3); + + rxMock.sendEventsFrom(SingleEvent.onNext(42)) + .to(foo); + + Integer result = foo.toBlocking().first(); + + assertThat(result).isEqualTo(42); + } + + @Test + public void itSendsEventsToMockedObservableAccordingToParameter() throws Exception { + Observable foo = mockedRepo.foo(3); + Observable bar = mockedRepo.foo(1); + + rxMock.sendEventsFrom(SingleEvent.onNext(42)) + .to(foo); + rxMock.sendEventsFrom(SingleEvent.onNext(24)) + .to(bar); + + Integer result = foo.toBlocking().first(); + Integer result2 = bar.toBlocking().first(); + + assertThat(result).isEqualTo(42); + assertThat(result2).isEqualTo(24); + } + + @Test + public void itDeterminesWetherAnObservableIsProvidedByAGivenRepository() throws Exception { + Observable foo = mockedRepo.foo(3); + + boolean result = rxMock.provides(foo); + boolean result2 = rxMock.provides(Observable.just(1)); + + assertThat(result).isTrue(); + assertThat(result2).isFalse(); + } + + @Test + public void itProvidesTheSameObservableForTheSameMethodParamCombination() throws Exception { + Observable foo = mockedRepo.foo(3); + Observable bar = mockedRepo.foo(3); + + assertThat(foo).isEqualTo(bar); + } + + @Test + public void resetMocksResetsPipelines() throws Exception { + Observable foo = mockedRepo.foo(3); + + rxMock.sendEventsFrom(SingleEvent.onNext(42)) + .to(foo); + + rxMock.resetMocks(); + + Observable bar = mockedRepo.foo(3); + + rxMock.sendEventsFrom(SingleEvent.onCompleted()) + .to(bar); + + Boolean result = bar.isEmpty().toBlocking().first(); + + assertThat(result).isTrue(); + } + + @Test + public void getEventsForDoesNotAffectSubscriptionToMockeObservables() throws Exception { + Observable foo = mockedRepo.foo(3); + + final Notification[] test = (Notification[]) Array.newInstance(Notification.class, 1); + rxMock.getEventsFor(foo) + .subscribe( + new Action1>() { + @Override + public void call(Notification integerNotification) { + test[0] = integerNotification; + } + }); + + rxMock.sendEventsFrom(SingleEvent.onNext(42)) + .to(foo); + + assertThat(test[0]).isNull(); + + Integer result = foo.toBlocking().first(); + + assertThat(test[0].getKind()).isEqualTo(Notification.Kind.OnNext); + assertThat(test[0].getValue()).isEqualTo(42); + } + + public interface TestRepository { + Observable foo(int bar); + } +} diff --git a/core/src/test/java/com/novoda/rxpresso/RxPressoTest.java b/core/src/test/java/com/novoda/rxpresso/RxPressoTest.java index 862b259..58f532e 100644 --- a/core/src/test/java/com/novoda/rxpresso/RxPressoTest.java +++ b/core/src/test/java/com/novoda/rxpresso/RxPressoTest.java @@ -2,19 +2,19 @@ import android.support.test.espresso.IdlingResource; -import com.novoda.rxmocks.RxMatcher; -import com.novoda.rxmocks.RxMocks; -import com.novoda.rxmocks.SimpleEvents; +import com.novoda.rxpresso.matcher.RxMatcher; +import com.novoda.rxpresso.mock.SingleEvent; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.mockito.Mockito; import rx.Notification; import rx.Observable; -import static com.novoda.rxmocks.RxExpect.any; +import static com.novoda.rxpresso.matcher.RxExpect.any; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -28,9 +28,9 @@ public class RxPressoTest { @Before public void setUp() throws Exception { - mockedRepo = RxMocks.mock(TestRepository.class); + mockedRepo = Mockito.mock(TestRepository.class); resourceCallback = mock(IdlingResource.ResourceCallback.class); - rxPresso = new RxPresso(mockedRepo); + rxPresso = RxPresso.from(mockedRepo); rxPresso.registerIdleTransitionCallback(resourceCallback); } @@ -39,7 +39,7 @@ public void itSendsEventsToMockedObservable() throws Exception { Observable foo = mockedRepo.foo(3); rxPresso.given(foo) - .withEventsFrom(SimpleEvents.onNext(42)) + .withEventsFrom(SingleEvent.onNext(42)) .expect(any(Integer.class)); Integer result = foo.toBlocking().first(); @@ -53,10 +53,10 @@ public void itSendsEventsToMockedObservableAccordingToParameter() throws Excepti Observable bar = mockedRepo.foo(1); rxPresso.given(foo) - .withEventsFrom(SimpleEvents.onNext(42)) + .withEventsFrom(SingleEvent.onNext(42)) .expect(any(Integer.class)); rxPresso.given(bar) - .withEventsFrom(SimpleEvents.onNext(24)) + .withEventsFrom(SingleEvent.onNext(24)) .expect(any(Integer.class)); Integer result = foo.toBlocking().first(); @@ -71,7 +71,7 @@ public void resetMocksResetsPipelines() throws Exception { Observable foo = mockedRepo.foo(3); rxPresso.given(foo) - .withEventsFrom(SimpleEvents.onNext(42)) + .withEventsFrom(SingleEvent.onNext(42)) .expect(any(Integer.class)); rxPresso.resetMocks(); @@ -79,7 +79,7 @@ public void resetMocksResetsPipelines() throws Exception { Observable bar = mockedRepo.foo(3); rxPresso.given(bar) - .withEventsFrom(SimpleEvents.onCompleted()) + .withEventsFrom(SingleEvent.onCompleted()) .expect( new RxMatcher>() { @Override @@ -104,7 +104,7 @@ public void idlingRessourceTransitionsToIdleWhenDataIsDelivered() throws Excepti Observable foo = mockedRepo.foo(3); rxPresso.given(foo) - .withEventsFrom(SimpleEvents.onNext(42)) + .withEventsFrom(SingleEvent.onNext(42)) .expect(any(Integer.class)); assertThat(rxPresso.isIdleNow()).isFalse(); @@ -120,7 +120,7 @@ public void itFailsIfNoEventMatchingMatcherIsReceived() throws Exception { Observable foo = mockedRepo.foo(3); rxPresso.given(foo) - .withEventsFrom(SimpleEvents.onCompleted()) + .withEventsFrom(SingleEvent.onCompleted()) .expect(any(Integer.class)); Integer result = foo.toBlocking().first(); diff --git a/demo/build.gradle b/demo/build.gradle index 830bd8c..f6cc8db 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -25,8 +25,8 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'io.reactivex:rxjava:1.0.10' - compile 'io.reactivex:rxandroid:0.24.0' + compile 'io.reactivex:rxjava:1.0.14' + compile 'io.reactivex:rxandroid:1.0.1' androidTestCompile 'com.android.support.test:runner:0.2' androidTestCompile 'com.android.support.test:rules:0.2' diff --git a/demo/src/androidTest/java/com/novoda/rxpresso/demo/SampleTest.java b/demo/src/androidTest/java/com/novoda/rxpresso/demo/SampleTest.java index e1f3931..e533a51 100644 --- a/demo/src/androidTest/java/com/novoda/rxpresso/demo/SampleTest.java +++ b/demo/src/androidTest/java/com/novoda/rxpresso/demo/SampleTest.java @@ -5,22 +5,20 @@ import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; -import com.novoda.rxmocks.RxMocks; -import com.novoda.rxmocks.SimpleEvents; import com.novoda.rxpresso.RxPresso; +import com.novoda.rxpresso.mock.SingleEvent; import java.io.IOException; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import static android.support.test.espresso.assertion.ViewAssertions.matches; -import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; -import static android.support.test.espresso.matcher.ViewMatchers.withId; -import static android.support.test.espresso.matcher.ViewMatchers.withText; -import static com.novoda.rxmocks.RxExpect.any; -import static com.novoda.rxmocks.RxExpect.anyError; +import static android.support.test.espresso.matcher.ViewMatchers.*; +import static com.novoda.rxpresso.matcher.RxExpect.any; +import static com.novoda.rxpresso.matcher.RxExpect.anyError; import static org.hamcrest.Matchers.containsString; @RunWith(AndroidJUnit4.class) @@ -35,10 +33,10 @@ public class SampleTest { protected void beforeActivityLaunched() { SampleApplication application = (SampleApplication) InstrumentationRegistry.getTargetContext().getApplicationContext(); - mockedRepo = RxMocks.mock(DataRepository.class); + mockedRepo = Mockito.mock(DataRepository.class); application.setRepository(mockedRepo); - rxPresso = new RxPresso(mockedRepo); + rxPresso = RxPresso.from(mockedRepo); Espresso.registerIdlingResources(rxPresso); } @@ -53,7 +51,7 @@ protected void afterActivityFinished() { @Test public void randomIntegerIsDisplayed() throws Exception { rxPresso.given(mockedRepo.getRandomNumber(10)) - .withEventsFrom(SimpleEvents.onNext(3)) + .withEventsFrom(SingleEvent.onNext(3)) .expect(any(Integer.class)) .thenOnView(withId(R.id.number)) .check(matches(withText(containsString(String.valueOf(3))))); @@ -62,7 +60,7 @@ public void randomIntegerIsDisplayed() throws Exception { @Test public void whenAnErrorOccursAnErrorDialogIsDisplayedShowingErrorMessage() throws Exception { rxPresso.given(mockedRepo.getRandomNumber(10)) - .withEventsFrom(SimpleEvents.onError(new IOException("Not random enough ?!"))) + .withEventsFrom(SingleEvent.onError(new IOException("Not random enough ?!"))) .expect(anyError(Integer.class, IOException.class)) .thenOnView(withText("Not random enough ?!")) .check(matches(isDisplayed())); diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0c71e76..e7faee0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml deleted file mode 100644 index 374d72a..0000000 --- a/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/java/com/novoda/rxpresso/Expect.java b/src/main/java/com/novoda/rxpresso/Expect.java deleted file mode 100644 index aa1dea1..0000000 --- a/src/main/java/com/novoda/rxpresso/Expect.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.novoda.rxpresso; - -import android.support.test.espresso.IdlingResource; - -import com.novoda.rxmocks.RxExpect; -import com.novoda.rxmocks.RxMatcher; -import com.novoda.rxmocks.RxMocks; - -import java.util.concurrent.atomic.AtomicBoolean; - -import rx.Notification; -import rx.Observable; -import rx.Subscription; -import rx.functions.Action1; -import rx.plugins.RxErrorRethrower; - -public class Expect implements IdlingResource { - - private final Object repo; - private final Observable observable; - private final Observable source; - private final AtomicBoolean idle = new AtomicBoolean(true); - - private Subscription subscription; - private ResourceCallback resourceCallback; - - public Expect(Object repo, Observable source, Observable observable) { - this.repo = repo; - this.source = source; - this.observable = observable; - } - - public Then expect(RxMatcher> matcher) { - expectAnyMatching(matcher); - RxMocks.with(repo) - .sendEventsFrom(source) - .to(observable); - return new Then(); - } - - private void expectAnyMatching(RxMatcher> matcher) { - RxErrorRethrower.register(); - idle.compareAndSet(true, false); - subscription = RxMocks.with(repo) - .getEventsFor(observable) - .subscribe(RxExpect.expect(matcher, new Action1>() { - @Override - public void call(Notification notification) { - subscription.unsubscribe(); - RxErrorRethrower.unregister(); - transitionToIdle(); - } - })); - } - - private void transitionToIdle() { - if (idle.compareAndSet(false, true)) { - resourceCallback.onTransitionToIdle(); - } - } - - @Override - public String getName() { - return "When"; - } - - @Override - public boolean isIdleNow() { - return idle.get(); - } - - @Override - public void registerIdleTransitionCallback(ResourceCallback resourceCallback) { - this.resourceCallback = resourceCallback; - } - -} diff --git a/src/main/java/com/novoda/rxpresso/RxPresso.java b/src/main/java/com/novoda/rxpresso/RxPresso.java deleted file mode 100644 index 9f905d0..0000000 --- a/src/main/java/com/novoda/rxpresso/RxPresso.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.novoda.rxpresso; - -import android.support.test.espresso.IdlingResource; - -import com.novoda.rxmocks.RxMocks; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import rx.Observable; -import rx.functions.Func1; - -public class RxPresso implements IdlingResource { - - private final Object[] repositories; - private final List pendingResources = Collections.synchronizedList(new ArrayList()); - - private ResourceCallback resourceCallback; - - public RxPresso(Object... repositories) { - this.repositories = repositories; - } - - public With given(Observable observable) { - Object repo = Observable.from(repositories).filter(provides(observable)).toBlocking().first(); - final With with = new With<>(repo, observable); - pendingResources.add(with); - with.registerIdleTransitionCallback(new ResourceCallback() { - @Override - public void onTransitionToIdle() { - pendingResources.remove(with); - if (pendingResources.isEmpty()) { - resourceCallback.onTransitionToIdle(); - } - } - }); - return with; - } - - @Override - public String getName() { - return "RxPresso"; - } - - @Override - public boolean isIdleNow() { - if (pendingResources.isEmpty()) { - return true; - } - return Observable.from(pendingResources).all(isIdle).toBlocking().first(); - } - - @Override - public void registerIdleTransitionCallback(ResourceCallback resourceCallback) { - this.resourceCallback = resourceCallback; - } - - public void resetMocks() { - for (Object repo : repositories) { - RxMocks.with(repo).resetMocks(); - } - } - - private static Func1 provides(final Observable observable) { - return new Func1() { - @Override - public Boolean call(Object repo) { - return RxMocks.with(repo).provides(observable); - } - }; - } - - private static Func1 isIdle = new Func1() { - @Override - public Boolean call(IdlingResource resource) { - return resource.isIdleNow(); - } - }; - -} diff --git a/src/main/java/com/novoda/rxpresso/Then.java b/src/main/java/com/novoda/rxpresso/Then.java deleted file mode 100644 index 980497a..0000000 --- a/src/main/java/com/novoda/rxpresso/Then.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.novoda.rxpresso; - -import android.support.test.espresso.DataInteraction; -import android.support.test.espresso.ViewInteraction; -import android.view.View; - -import org.hamcrest.Matcher; - -import static android.support.test.espresso.Espresso.onData; -import static android.support.test.espresso.Espresso.onView; - -public class Then { - - public ViewInteraction thenOnView(Matcher viewMatcher) { - return onView(viewMatcher); - } - - public DataInteraction thenOnData(Matcher dataMatcher) { - return onData(dataMatcher); - } - -} diff --git a/src/main/java/com/novoda/rxpresso/With.java b/src/main/java/com/novoda/rxpresso/With.java deleted file mode 100644 index 732b872..0000000 --- a/src/main/java/com/novoda/rxpresso/With.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.novoda.rxpresso; - -import android.support.test.espresso.IdlingResource; - -import rx.Observable; - -public class With implements IdlingResource { - - private final Object repo; - private final Observable observable; - private ResourceCallback resourceCallback; - private Expect expect; - - public With(Object repo, Observable observable) { - this.repo = repo; - this.observable = observable; - } - - public Expect withEventsFrom(Observable source) { - expect = new Expect<>(repo, source, observable); - expect.registerIdleTransitionCallback(new ResourceCallback() { - @Override - public void onTransitionToIdle() { - resourceCallback.onTransitionToIdle(); - } - }); - return expect; - } - - @Override - public String getName() { - return "With"; - } - - @Override - public boolean isIdleNow() { - return expect == null || expect.isIdleNow(); - } - - @Override - public void registerIdleTransitionCallback(ResourceCallback resourceCallback) { - this.resourceCallback = resourceCallback; - } - -} diff --git a/src/main/java/rx/plugins/RxErrorRethrower.java b/src/main/java/rx/plugins/RxErrorRethrower.java deleted file mode 100644 index b78a809..0000000 --- a/src/main/java/rx/plugins/RxErrorRethrower.java +++ /dev/null @@ -1,33 +0,0 @@ -package rx.plugins; - -public final class RxErrorRethrower { - - private RxErrorRethrower() { - } - - public static void register() { - RxJavaPlugins instance = RxJavaPlugins.getInstance(); - if (!(instance.getErrorHandler() instanceof RethrowerJavaErrorHandler)) { - unregister(); - instance.registerErrorHandler(new RethrowerJavaErrorHandler()); - } - } - - public static void unregister() { - RxJavaPlugins instance = RxJavaPlugins.getInstance(); - RxJavaSchedulersHook schedulersHook = instance.getSchedulersHook(); - RxJavaObservableExecutionHook observableExecutionHook = instance.getObservableExecutionHook(); - instance.reset(); - instance.registerObservableExecutionHook(observableExecutionHook); - instance.registerSchedulersHook(schedulersHook); - } - - private static class RethrowerJavaErrorHandler extends RxJavaErrorHandler { - @Override - public void handleError(Throwable e) { - if (e instanceof RuntimeException) { - throw (RuntimeException) e; - } - } - } -}