Muszę wykonać testy jednostkowe metod klasy Singleton, która wewnętrznie korzysta z RxJava Singles i używa struktury testowej PowerMock do mockowania statycznej klasy i metod. Próbowałem różnych metod, aby mockować metody Schedulers.io () i AndroidSchedulers.mainThread (), ale to nie działa. Otrzymuję błąd java.lang.NullPointerException w wierszu .subscribeOn (Schedulers.io ()) w metodzie UserApi.verifyUserData () .

Singleton Class UserApi (testowana klasa)

final public class UserApi {
    private CompositeDisposable compositeDisposable;
    private String userID; 
    //private final SchedulerProvider schedulerProvider;

    private UserApi(String userId) {
        super();
        this.userID = userId;
        //this.schedulerProvider = schedulerProvider;
    }

    public static UserApi getInstance() {
        return SingletonHolder.sINSTANCE;
    }

    private static final class SingletonHolder {
        private static final UserApi sINSTANCE;

        static {
            String uuid = UUID.randomUUID().toString();
            sINSTANCE = new UserApi(uuid);
        }
    }

    // Rest Api call

    public void verifyUserData(byte[] doc, byte[] img) {
        this.compositeDisposable = new CompositeDisposable();
        String docStr = Base64.encodeToString(doc, Base64.NO_WRAP);
        String imgStr = Base64.encodeToString(img, Base64.NO_WRAP);

        final Disposable apiDisposable = IdvManager.getInstance().getUserManager().verifyUserData(docStr, imgStr)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<JsonObject>() {
                    @Override
                    public void accept(JsonObject verifyResponse) throws Exception {
                        pollResult();
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(Throwable error) throws Exception {
                      // handle error code...
                    }
                });
        this.compositeDisposable.add(apiDisposable);
    }

    private void pollResult() {
        // code here...
    }

}

Klasa i interfejs UserManager

public interface UserManager {

    Single<JsonObject> verifyUserData(String docStr, String imgStr);

}

final class UserManagerImpl implements UserManager {

    private final UserService userService;

    UserManagerImpl(final Retrofit retrofit) {
        super();
        this.userService = retrofit.create(UserService.class);
    }
    @Override
    public Single<JsonObject> verifyUserData(String docStr, String imgStr) {
     // Code here...
    }
}

Test jednostkowy

@RunWith(PowerMockRunner.class)
@PrepareForTest({IdvManager.class, Base64.class, Schedulers.class, AndroidSchedulers.class, UserApi.class})
public class UserApiTest {

    @Mock
    public UserManager userManager;
    @Mock
    private Handler handler;

    private IdvManager idvManager;
    private Schedulers schedulers;

    private UserApi spyUserApi;
    private TestScheduler testScheduler;
    private String userID;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        testScheduler = new TestScheduler();
        handler = new Handler();
        PowerMockito.suppress(constructor(IdvManager.class));
        // mock static
        PowerMockito.mockStatic(IdvManager.class);
        PowerMockito.mockStatic(Schedulers.class);
        PowerMockito.mockStatic(AndroidSchedulers.class);
        PowerMockito.mockStatic(Base64.class);
        // Create mock for class
        idvManager = PowerMockito.mock(IdvManager.class);
        schedulers = PowerMockito.mock(Schedulers.class);
        PowerMockito.when(IdvManager.getInstance()).thenReturn(IdvManager);
        when(idvManager.getUserManager()).thenReturn(userManager);

        spyUserApi = PowerMockito.spy(UserApi.getInstance());
        // TestSchedulerProvider testSchedulerProvider = new TestSchedulerProvider(testScheduler);

        when(Base64.encodeToString((byte[]) any(), anyInt())).thenAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                return java.util.Base64.getEncoder().encodeToString((byte[]) invocation.getArguments()[0]);
            }
        });

        when(schedulers.io()).thenReturn(testScheduler);
        when(AndroidSchedulers.mainThread()).thenReturn(testScheduler);
        userID = UUID.randomUUID().toString();
    }

    @After
    public void clearMocks() {
        //Mockito.framework().clearInlineMocks();
    }

    @Test
    public void verifyUserData_callsPollResult_returnsResponse() {
        // Input
        String docStr = "iVBORw0KGgoAAAANSUhEUgAAAJ4AAACeCAYAAADDhbN7AA.....";
        // Output
        JsonObject verifyResponse = new JsonObject();
        verifyResponse.addProperty("status", "Response created");
        doReturn(Single.just(verifyResponse)).when(userManager).verifyUserData(docStr, docStr);
        // spy method call
        spyUserApi.verifyUserData(docFrontArr, docFrontArr);
        testScheduler.triggerActions();
        // assert
        verify(userManager).verifyUserData(docStr, docStr);

    }
}

Błąd

java.lang.NullPointerException
    at com.rahul.manager.UserApi.verifyUserData(UserApi.java:60)
    at com.rahul.manager.UserApiTest.verifyUserData_callsPollResult_returnsResponse(UserApiTest.java:171)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)

Nie jestem pewien, czy mogę przetestować metody klasy Singleton, szpiegując rzeczywiste wystąpienie klasy Singleton za pomocą PowerMock.

-1
Rahul Kumar 19 listopad 2019, 16:41
Spróbuj dodać UserAPI.class w adnotacji przygotowania
 – 
Sergio Arrighi
19 listopad 2019, 16:50
Dodałem, że faktycznie zrobił błąd literowy podczas publikowania, teraz poprawione.
 – 
Rahul Kumar
19 listopad 2019, 16:54
Czy UserManager jest jedną z Twoich zajęć? Jeśli to, co mówisz, jest poprawne, to verifyUserData zwróci null, co jest albo z powodu niezgodnych parametrów zdefiniowanych dla makiety, albo dlatego, że zmienna userManager to null. Możesz usunąć z zajęć i testu wszystko, co nie jest powiązane, a następnie opublikować minimalny powtarzalny przykład.
 – 
second
19 listopad 2019, 17:17
UserManger to interfejs zaimplementowany przez odpowiednią klasę i wyśmiewałem to samo, dodałem również wymagany kod w powyższym poście. Więc to nie stwarza żadnego problemu. W rzeczywistości błąd nullPointerException pojawia się, gdy metoda Schedulers.io() jest wywoływana przez metodę RxJava Single. Nie jestem pewien, czy mogę przetestować metodę klasy singleton, szpiegując rzeczywistą instancję UserApi po wywołaniu metody UserApi.getInstance().
 – 
Rahul Kumar
19 listopad 2019, 18:46

2 odpowiedzi

Testowanie kodu jest złożone, ponieważ nie można go testować ani rozszerzać. Zawiera wszędzie zakodowane zależności (np. identyfikator użytkownika, program obsługi, kilka singli).
Jeśli zdecydujesz się użyć innego podejścia do generowania identyfikatorów lub innego modułu obsługi, nie będziesz w stanie tego zrobić bez przepisania całej klasy.
Zamiast sztywno kodować zależności, poproś o nie w konstruktorze (dla zależności obowiązkowych) lub setters (dla opcjonalnych).
Dzięki temu Twój kod będzie rozszerzalny i testowalny. Po wykonaniu tej czynności zobaczysz, że twoja klasa zawiera kilka obowiązków, po przeniesieniu ich do osobnych klas uzyskasz znacznie lepszy obraz :-)

Na przykład:

public UserApi(String userId, Handler handle) {
    this.userId = userId;
    this.handler = handler;
}
0
nickolay.laptev 19 listopad 2019, 17:03
1
To wydaje się być komentarzem, a nie odpowiedzią.
 – 
second
19 listopad 2019, 17:21
Usunąłem program obsługi i inny dodatkowy kod i zaktualizowałem to samo w powyższym poście. Mój problem jest inny, co wynika z tego, że Schedulers.io() jest przekazywana w metodzie subscribeOn() RxJava Single. Nie jestem pewien, czy po wywołaniu metody UserApi.getInstance() mogę przetestować metodę klasy singleton, szpiegując rzeczywistą instancję klasy UserApi.
 – 
Rahul Kumar
19 listopad 2019, 18:50
Problem z tym kodem, który widzę, polega na tym, że w jednej klasie jest za dużo. Singletony nie są złe, ale wywoływanie ich w sposób ukryty sprawia, że ​​testowanie i rozszerzanie tej klasy jest niezwykle trudne. Idealnie byłoby, gdybyśmy nie używali słowa kluczowego „new” w naszych metodach. Metoda UserApi#verifyUserData wymaga UserManager. Zamiast pobierać go samemu z singletona lub jakiejś statycznej rzeczy, po prostu poproś o to w konstruktorze UserApi. Obowiązkiem użytkowników UserApi jest przekazanie odpowiedniej instancji UserManager. Aby uzyskać szczegółowe informacje, zobacz misko.hevery.com/code-reviewers-guide Misko uczył Googlersów korzystać z TDD.
 – 
nickolay.laptev
19 listopad 2019, 21:57

Schedulers.io() jest metodą statyczną, więc musisz użyć mockStatic (co zrobiłeś) i odpowiednio zdefiniować powiązany mock.

Trochę przestawiłem Twoją metodę setup, aby poprawić czytelność i naprawiłem błąd. Nie potrzebujesz instancji Schedulers (zmienna, którą nazwałeś schedulers).

Prawdopodobnie prosta literówka, którą zrobiłeś, ponieważ zrobiłeś właściwą rzecz dla Base64 i AndroidSchedulers.

@Before
public void setUp() {

    MockitoAnnotations.initMocks(this);

    testScheduler = new TestScheduler();
    handler = new Handler();

    // mock for instance of `IdvManager` 
    PowerMockito.suppress(constructor(IdvManager.class));
    idvManager = PowerMockito.mock(IdvManager.class);
    when(idvManager.getUserManager()).thenReturn(userManager);

    // mock for `IdvManager` class
    PowerMockito.mockStatic(IdvManager.class);
    PowerMockito.when(IdvManager.getInstance()).thenReturn(idvManager);

    // mock for `Schedulers` class
    PowerMockito.mockStatic(Schedulers.class);
    when(Schedulers.io()).thenReturn(testScheduler);

    // spy for instance of `UserApi`
    spyUserApi = PowerMockito.spy(UserApi.getInstance());

    // mock for `Base64` class
    PowerMockito.mockStatic(Base64.class);
    when(Base64.encodeToString((byte[]) any(), anyInt())).thenAnswer(new Answer<Object>() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            return java.util.Base64.getEncoder().encodeToString((byte[]) invocation.getArguments()[0]);
        }
    });

    // mock for `AndroidSchedulers` class
    PowerMockito.mockStatic(AndroidSchedulers.class);
    when(AndroidSchedulers.mainThread()).thenReturn(testScheduler);

    userID = UUID.randomUUID().toString();
}

Jednak w NPE brakuje części, która faktycznie wskazuje na jego awarię, rozważ dodanie jej, jeśli to nie rozwiąże problemu.

0
second 19 listopad 2019, 22:21