2017-08-04 19 views
1

Ich habe Probleme beim Testen meines RxJava-Codes, da er immer den Test zu durchlaufen scheint, ohne auf die Ausführung zu warten, die beim Aufruf von subscribe() passieren sollte. RxJava Unittests

Dies ist das Repository zu testen:

public class Repository<T extends Entity> { 

    private final DataSource<T> remoteDataSource; 
    private final DataSource<T> localDataSource; 

    public Repository(DataSource<T> remoteDataSource, DataSource<T> localDataSource) { 
     this.remoteDataSource = remoteDataSource; 
     this.localDataSource = localDataSource; 
    } 

    public Observable<Boolean> save(T entity) { 

     return localDataSource 
       .save(entity) 
       .flatMap(success -> { 
        if (success) { 
         return remoteDataSource.save(entity); 
        } 
        return Observable.just(Boolean.FALSE); 
       }) 
       .doOnNext(success -> { 
        if (success) { 
         if (cache == null) { 
          cache = new LinkedHashMap<>(); 
         } 
         cache.put(entity.getId(), entity); 
        } 
       }); 
    } 
} 

Dies ist die Datasource-Schnittstelle:

public interface DataSource<T extends Entity> { 
    <T> Observable<T> get(); 

    Observable<Boolean> save(T entity); 

    Observable<Boolean> clear(); 

    Observable<Boolean> remove(T entity); 
} 

Und dies ist der Unit-Test eines MockDataSource enthält, die den Datenzugriff mit verschiedener Ausführung ist sollte simulieren Timings:

public class RepositoryTest { 

    @Test 
    public void testRepository() { 

     MockSource remoteSource = new MockSource("RemoteSource", 1000L); 
     MockSource localSource = new MockSource("LocalSource", 200L); 
     Repository<Poi> poiRepository = new Repository<>(remoteSource, localSource); 

     Poi poi1 = newMockPoi(); 

     Observable<Boolean> obs = poiRepository.save(poi1); 
     TestSubscriber<Boolean> testSubscriber = new TestSubscriber<>(); 
     obs.subscribe(testSubscriber); 

     testSubscriber.assertNoErrors(); 
     testSubscriber.assertReceivedOnNext(Arrays.asList(true)); 


    } 

    private Poi newMockPoi() { 
     Poi poi = new Poi(); 
     poi.name = RandomStringUtils.randomAlphabetic(12); 
     poi.description = RandomStringUtils.randomAlphabetic(255); 
     poi.latitude = new Random().nextDouble(); 
     poi.longitude = new Random().nextDouble(); 
     return poi; 
    } 


    private class Poi extends Entity { 
     String name; 
     String description; 
     Double latitude; 
     Double longitude; 
    } 

    private class MockSource implements DataSource<Poi> { 

     private String name; 
     private final long delayInMilliseconds; 

     private Map<Long, Poi> pois = new LinkedHashMap<>(); 

     private MockSource(String name, long delayInMilliseconds) { 
      this.delayInMilliseconds = delayInMilliseconds; 
      this.name = name; 
     } 

     @Override 
     public Observable<List<Poi>> get() { 
      return Observable 
        .zip(
          Observable 
            .just(pois) 
            .map(Map::entrySet) 
            .flatMapIterable(entries -> entries) 
            .map(Map.Entry::getValue) 
            .toList(), 
          Observable 
            .interval(delayInMilliseconds, TimeUnit.MILLISECONDS), (obs, timer) -> obs) 
        .doOnNext(pois -> System.out.println("Soure " + name + " emitted entity")); 
     } 

     @Override 
     public Observable<Boolean> save(Poi entity) { 
      return Observable 
        .zip(
          Observable.just(true).asObservable(), 
          Observable.interval(delayInMilliseconds, TimeUnit.MILLISECONDS), (obs, timer) -> obs) 
        .doOnNext(value -> pois.put(entity.getId(), entity)) 
        .doOnNext(pois -> System.out.println("Soure " + name + " saved entity")); 
     } 

     @Override 
     public Observable<Boolean> clear() { 
      return Observable 
        .zip(
          Observable.just(true).asObservable(), 
          Observable.interval(delayInMilliseconds, TimeUnit.MILLISECONDS), (obs, timer) -> obs) 
        .doOnNext(value -> pois.clear()) 
        .doOnNext(pois -> System.out.println("Soure " + name + " cleared all entities")); 
     } 

     @Override 
     public Observable<Boolean> remove(Poi entity) { 
      return Observable 
        .zip(
          Observable.just(true).asObservable(), 
          Observable.interval(delayInMilliseconds, TimeUnit.MILLISECONDS), (obs, timer) -> obs) 
        .doOnNext(value -> pois.remove(entity)) 
        .doOnNext(pois -> System.out.println("Soure " + name + " removed entity")); 
     } 
    } 
} 

Dies ist der Ausgang:

java.lang.AssertionError: Number of items does not match. Provided: 1 
Actual: 0. 
Provided values: [true] 
Actual values: [] 
(0 completions) 

at rx.observers.TestSubscriber.assertionError(TestSubscriber.java:667) 
at rx.observers.TestSubscriber.assertReceivedOnNext(TestSubscriber.java:320) 
at nl.itc.geofara.app.data.source.RepositoryTest.testRepository(RepositoryTest.java:37) 

Das Setzen von Haltepunkten in der Speichermethode des Repositorys und das Ausführen im Debug-Modus zeigen auch, dass der Code innerhalb von z. .flatMap (Erfolg -> ...) wird nie aufgerufen.

+0

Siehe https://StackOverflow.com/a/39828581/4191629, "Problembehandlung", "Race-Bedingung" – maciekjanusz

+0

Ich habe die Schedulers.immiate() Begriff zuvor gesehen, die angegebene Quelle ruft jedoch nie subscribeOn() überall auf. Ich dachte, dass alle Aufgaben jetzt auf demselben Thread ausgeführt werden wie der Komponententest selbst. Liege ich falsch in dieser Annahme? –

+0

Ok sh * t ... Sie haben Recht! –

Antwort

2

Sie erleben eine Race-Bedingung zwischen der beobachtbaren Ausführung und dem JUnit-Thread, weil Observable.interval, die Sie in Ihrer Mock Quelle verwenden führen implicit subscription on computation scheduler:

Scheduler: Intervall auf dem standardmäßig arbeitet Berechnung Scheduler.

Siehe auch weitere Erläuterungen in this answer, "Fehlersuche", "Race Condition".