void start() {
    bar.foo()
        .filter(i -> i % 2 == 0)
        .subscribeOn(computation())
        .observeOn(io())
        .subscribe(new FooSubscriber());
}

En esta función veo 3 puntos para probar:

  1. Verifica que llamo a bar.foo().
  2. Verifique que el filter esté implementado correctamente.
  3. Verifique que esté suscrito a bar.foo().

El primer punto es fácil de probar con Mockito.verify(). El tercer punto puedo inyectar los Programadores y usar Schedulers.immediate() y luego burlarme del observador con un Sujeto y marcar Subject.hasObservers(). Pero no tengo idea de cómo probar el segundo punto.

¿Cómo puedo probar este código? ¿Debo refactorizarlo? ¿Cómo?

Por favor, piense que filter es solo un ejemplo, tengo una gran cadena con diferentes operadores.

1
Brais Gabin 30 dic. 2016 a las 15:08
Nunca verificaría que llamas a bar.foo(); está ahí como lo primero en tu código, si eso no funciona cuando llamas a start() tienes problemas mucho mayores de los que preocuparte. (a menos que no sea lo primero ...)
 – 
Tassos Bassoukos
31 dic. 2016 a las 15:09

1 respuesta

La mejor respuesta

Es difícil probar este método ya que no hay un comportamiento "observable" para afirmar y necesita obtener su código de prueba "en el camino" de la lógica.

Aquí hay un enfoque simple que puede seguir: (aunque es posible que desee considerar dividir las cosas para facilitar las pruebas)

  1. Simulacro de foo, verifique que se llame a bar () si es necesario, devuelva un Observable real de bar () que desenrollará la cadena de devolución de llamada cuando se llame a subscribe. - esto probará que su cadena está conectada como se esperaba.

  2. Inyecte programadores que ejecuten la lógica en el hilo principal de manera bloqueante, manteniendo así las pruebas sincronizadas y fáciles de entender.

  3. Extraiga new FooSubscriber() a un método privado de paquete y use Mockito para espiar el nuevo método, devolviendo un suscriptor de prueba que haga afirmaciones sobre los datos filtrados emitidos por el observable, o inyecte una clase de fábrica que construya instancias de FooSubscriber que pueda simulacro con fines de prueba al devolver un suscriptor de prueba. - Básicamente, el uso codificado de la nueva palabra clave le impide probar el comportamiento.

Puedo dar un ejemplo si lo necesita, espero que esto lo ponga en marcha.

EDITAR: ejemplo de ambos métodos descritos anteriormente:

package com.rx;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import rx.Observable;
import rx.Observer;
import rx.Scheduler;
import rx.schedulers.Schedulers;

import java.util.ArrayList;
import java.util.List;

@RunWith(MockitoJUnitRunner.class)
public class TestRxMethod {

    // prod Bar class - this class tested in isolation in different test.
    public static class Bar {

        public Observable<Integer> foo() {
            return null;
        }
    }

    // prod FooSubscriber class - this class tested in isolation in different test.
    public static class FooSubscriber implements Observer<Integer> {

        @Override
        public void onCompleted() {
        }

        @Override
        public void onError(Throwable e) {
        }

        @Override
        public void onNext(Integer t) {
        }
    }

    // prod FooSubscriberFactory class - this class tested in isolation in different test.
    public static class FooSubscriberFactory {

        public Observer<Integer> getInstance() {
            return null;
        }
    }

    // prod "class under test"
    public static class UnderTest {
        private final Bar bar;
        private final Scheduler computationScheduler;
        private final Scheduler ioScheduler;
        private final FooSubscriberFactory fooSubscriberFactory;

        public UnderTest(Bar bar, Scheduler computationScheduler, Scheduler ioScheduler,
                FooSubscriberFactory fooSubscriberFactory) {
            this.bar = bar;
            this.computationScheduler = computationScheduler;
            this.ioScheduler = ioScheduler;
            this.fooSubscriberFactory = fooSubscriberFactory;
        }

        public void start() {
            //@formatter:off
            bar.foo()
                .filter(i -> i.intValue() % 2 == 0)
                .subscribeOn(computationScheduler)
                .observeOn(ioScheduler)
                .subscribe(fooSubscriber());
            //@formatter:on
        }

        // package private so can be overridden by unit test some drawbacks
        // using this strategy like class cant be made final. - use only
        // if cant restructure code.
        Observer<Integer> fooSubscriber() {
            return fooSubscriberFactory.getInstance();
        }
    }

    // test Foo subscriber class - test will put set an instance of
    // this class as the observer on the callback chain.
    public static class TestFooSubscriber implements Observer<Integer> {

        public List<Integer> filteredIntegers = new ArrayList<>();

        @Override
        public void onCompleted() {
            // noop
        }

        @Override
        public void onError(Throwable e) {
            // noop
        }

        @Override
        public void onNext(Integer i) {
            // aggregate filtered integers for later assertions
            filteredIntegers.add(i);
        }
    }

    // mock bar for test
    private Bar bar;

    // mock foo subscriber factory for test
    private FooSubscriberFactory fooSubscriberFactory;

    // class under test - injected with test dependencies
    private UnderTest underTest;

    @Before
    public void setup() {
        bar = Mockito.mock(Bar.class);
        fooSubscriberFactory = Mockito.mock(FooSubscriberFactory.class);
        underTest = new UnderTest(bar, Schedulers.immediate(), Schedulers.immediate(), fooSubscriberFactory);
    }

    // Option #1 - injecting a factory
    @Test
    public void start_shouldWork_usingMockedFactory() {
        // setup bar mock to emit integers
        Mockito.when(bar.foo()).thenReturn(Observable.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));

        // setup the subscriber factory to produce an instance of the test subscriber
        TestFooSubscriber testSubscriber = new TestFooSubscriber();
        Mockito.when(fooSubscriberFactory.getInstance()).thenReturn(testSubscriber);

        underTest.start();

        Assert.assertEquals(5, testSubscriber.filteredIntegers.size());
        // ... add more assertions as needed per the use cases ...
    }

    // Option #2 - spying a protected method
    @Test
    public void start_shouldWork_usingSpyMethod() {
        // setup bar mock to emit integers
        Mockito.when(bar.foo()).thenReturn(Observable.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));

        // spy the class under test (use only as a last resort option)
        underTest = Mockito.spy(underTest);
        TestFooSubscriber testSubscriber = new TestFooSubscriber();
        Mockito.when(underTest.fooSubscriber()).thenReturn(testSubscriber);

        underTest.start();

        Assert.assertEquals(5, testSubscriber.filteredIntegers.size());
        // ... add more assertions as needed per the use cases ...
    }
}
1
Bob Lukens 3 ene. 2017 a las 01:26
Hablas de romper cosas para facilitar la prueba. ¿Cómo romperías este código para hacerlo más fácil?
 – 
Brais Gabin
3 ene. 2017 a las 16:00