Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
mishkaowner opened this issue Sep 5, 2017 · 2 comments
Open

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

mishkaowner opened this issue Sep 5, 2017 · 2 comments

Comments

@mishkaowner
Copy link

mishkaowner commented Sep 5, 2017

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 mishkaowner changed the title [Request] Please add JUnit test for the sample project [Request] Please add a unit test for the sample project Sep 5, 2017
@manas-chaudhari
Copy link
Owner

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.

@mishkaowner
Copy link
Author

Thank you for providing a simple solution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants