android-mvvm icon indicating copy to clipboard operation
android-mvvm copied to clipboard

[Request] Please add a unit test for the sample project

Open mishkaowner opened this issue 7 years ago • 2 comments

Hi, your MVVM design is very inspiring.

I made a small sample app which utilizes your modules and Kotlin.

Here is the link https://github.com/mishkaowner/MVVMSample

The only issue I came across is that you are missing a unit test example for your sample application.

I already checked all the test files

ex) https://github.com/manas-chaudhari/android-mvvm/blob/master/android-mvvm/src/test/java/com/manaschaudhari/android_mvvm/FieldUtilsTest.java

But I have not found a clear example of a unit test for ViewModel.

val vm = MainViewModel() vm.edit.set("Hello") vm.result.toObservable().test().assertValue("You typed Hello")

this is the best test code I made so far...

Please, lead me to the right direction.

Thank you

mishkaowner avatar Sep 05 '17 05:09 mishkaowner

I like to keep the unit tests of ViewModels simple without Rx dependencies.

LoginViewModel loginViewModel = new LoginViewModel();

loginViewModel.email.set("invalid@invalid");

assertEquals("Invalid email", loginViewModel.emailError.get());

However, the dependent values (emailError in this case) only update after they have been subscribed, which data binding does when the app runs. So, for testing, it will be required for us to setup these subscriptions.

The test() method in your code does exactly the same. Maybe we can write a function test that takes an ObservableField and returns a TestObserver to reduce the repetition.

Then the code will become test(vm.result).assertValue("You typed Hello");

Other way is to write a @Before method and subscribe to all the ObservableFields in the viewModel. Subscribing is simply invoking addOnPropertyChangedCallback function with an empty callback. Also, we can setup mocks for dependencies here so that it is convenient to assert in unit tests.

This example should give you a good idea.


public class LoginViewModelTest {

    private LoginViewModel sut;
    private Api mockApi;
    private BehaviorSubject<Result<LoginResult>> apiDriver;
    private Navigator mockNavigator;
    private LoginResult successResult;
    private Session mockSession;
    private BehaviorSubject<Result<Object>> generateOtpApiDriver;
    private MessageHelper mockMessageHelper;

    @Before
    public void setUp() throws Exception {
        mockSession = mock(Session.class);
        mockMessageHelper = mock(MessageHelper.class);
        successResult = new LoginResult(10, "some_token");
        mockApi = mock(Api.class);
        apiDriver = BehaviorSubject.create();
        generateOtpApiDriver = BehaviorSubject.create();
        when(mockApi.login(any(LoginRequest.class))).thenReturn(apiDriver.firstOrError());
        when(mockApi.generateOtp(any(OtpRequest.class))).thenReturn(generateOtpApiDriver.firstOrError());
        mockNavigator = mock(Navigator.class);
        sut = new LoginViewModel(mockApi, mockNavigator, mockSession, mockMessageHelper);
        subscribe(sut.getEmailError(), sut.getPasswordError());
    }

    @Test
    public void email_valid() throws Exception {
        sut.getEmail().set("[email protected]");
        sut.getLoginClick().run();

        assertEquals("", sut.getEmailError().get());
    }

    @Test
    public void email_invalid() throws Exception {
        sut.getEmail().set("abc_co");
        assertEquals("", sut.getEmailError().get());

        sut.getLoginClick().run();

        assertEquals("Invalid email format", sut.getEmailError().get());
    }

    @Test
    public void password_valid() throws Exception {
        sut.getPassword().set("abc_co");
        sut.getLoginClick().run();

        assertEquals("", sut.getPasswordError().get());
    }

    @Test
    public void password_invalid() throws Exception {
        sut.getPassword().set("");
        assertEquals("", sut.getPasswordError().get());

        sut.getLoginClick().run();

        assertEquals("Password cannot be empty", sut.getPasswordError().get());
    }

    @Test
    public void loginClick_ShouldNotInvokeLoginApi_invalidInput() throws Exception {
        sut.getEmail().set("testcom");
        sut.getPassword().set("testpass");
        sut.getLoginClick().run();

        verifyNoMoreInteractions(mockApi);
    }


    @Test
    public void loginClick_ShouldInvokeLoginApi_validInput() throws Exception {
        sut.getEmail().set("[email protected]");
        sut.getPassword().set("testpass");
        sut.getLoginClick().run();

        verify(mockApi).login(argThat(new ArgumentMatcher<LoginRequest>() {
            @Override
            public boolean matches(LoginRequest argument) {
                return argument.email.equals("[email protected]") &&
                        argument.password.equals("testpass");
            }
        }));
    }

    @Test
    public void navigateToHome_onLoginSuccess() throws Exception {
        sut.getEmail().set("[email protected]");
        sut.getPassword().set("123");
        sut.getLoginClick().run();

        apiDriver.onNext(ResultFactory.success(successResult));

        verify(mockNavigator).navigateToHome();
    }

    @Test
    public void storesAccessToken_onLoginSuccess() throws Exception {
        sut.getEmail().set("[email protected]");
        sut.getPassword().set("123");
        sut.getLoginClick().run();

        apiDriver.onNext(ResultFactory.success(successResult));

        verify(mockSession).storeAccessToken(successResult.userId, successResult.accessToken);
    }

    @Test
    public void displaysError_onLoginFailure() throws Exception {
        TestObserver<String> errorObserver = sut.getLoadingVM().errorMessage.test();

        sut.getEmail().set("[email protected]");
        sut.getPassword().set("123");
        sut.getLoginClick().run();

        apiDriver.onNext(ResultFactory.<LoginResult>validationError("Not found"));

        verify(mockNavigator, never()).navigateToHome();
        errorObserver.assertValue("Not found");
    }

    @Test
    public void displaysProgress_duringLogin() throws Exception {
        sut.getEmail().set("[email protected]");
        sut.getPassword().set("123");
        assertFalse(sut.getLoadingVM().progressVisible.get());

        sut.getLoginClick().run();

        assertTrue(sut.getLoadingVM().progressVisible.get());

        apiDriver.onNext(ResultFactory.<LoginResult>httpErrorUnknown());

        assertFalse(sut.getLoadingVM().progressVisible.get());
    }

    @Test
    public void generateOneTimePassword_invokesApi() throws Exception {
        sut.getEmail().set("[email protected]");
        sut.getGenerateOTPClick().run();

        verify(mockApi).generateOtp(argThat(new ArgumentMatcher<OtpRequest>() {
            @Override
            public boolean matches(OtpRequest argument) {
                return argument.email.equals("[email protected]");
            }
        }));
    }

    @Test
    public void displaysMessage_onSuccess() throws Exception {
        sut.getEmail().set("[email protected]");
        sut.getGenerateOTPClick().run();
        verifyNoMoreInteractions(mockMessageHelper);

        generateOtpApiDriver.onNext(ResultFactory.success(new Object()));

        verify(mockMessageHelper).showMessage("Check email for OTP");
    }

    @Test
    public void displaysProgressAndError_onFailure() throws Exception {
        ObservableField<Boolean> progress = sut.getLoadingVM().progressVisible;
        TestObserver<String> errorObserver = sut.getLoadingVM().errorMessage.test();
        assertFalse(progress.get());

        sut.getGenerateOTPClick().run();

        assertTrue(progress.get());

        generateOtpApiDriver.onNext(ResultFactory.httpErrorUnknown());

        assertFalse(progress.get());
        errorObserver.assertValue("Something went wrong");
        verifyNoMoreInteractions(mockMessageHelper);
    }

    @Test
    public void signup_navigatesToAddUser() throws Exception {
        sut.getSignupClick().run();

        verify(mockNavigator).navigateToAddUser();
    }
}

Lets keep this issue open as it is important to have an example in the sample app.

manas-chaudhari avatar Sep 07 '17 12:09 manas-chaudhari

Thank you for providing a simple solution!

mishkaowner avatar Sep 08 '17 15:09 mishkaowner