Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test MVVM with DataBinding on Android

I been searching online but I'm unable to find out how testing is made better with MVVM. I get the idea of having a viewModel which interfaces with the view but I don't know how I would write good test cases with MVVM. I have the following ViewModel in Android already:

public class ViewModel extends BaseObservable {
    private long countDownTime;
    private MyCountDownTimer mCountDownTimer;
    private final String TAG = getClass().getSimpleName();

    @Bindable    
    public long getCountDownTime() {
        return countDownTime;
    }


    public void setCountDownTime(long countDownTime) {
        this.countDownTime = countDownTime;

        notifyPropertyChanged((int) BR.countDownTime);
        Log.d(TAG,"prime tick:"+countDownTime);
    }

    public void startCounting(Long milli){
        mCountDownTimer.restartTimer(milli);
    }
}

and then I have a xml view that uses it. I also have an activity which actually binds the xml to this view. This activity looks like this:

public class MainActivity extends FragmentActivity {
    CountdownBinder mCountdownBinder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //setContentView(R.layout.activity_main);
        mCountdownBinder = DataBindingUtil.setContentView(this, R.layout.activity_main);

        //Lets reference our textview just for fun
        mCountdownBinder.tvGreen.setText("initial text");
        ViewModel viewModel = ViewModel.instance();

        //now tell databinding about your viewModel below
        mCountdownBinder.setViewModel(viewModel);
        viewModel.startCounting(200000L);
    }
}

Now I'm so confused how to this makes testing better. I read about it but I need a real world example. This code is from the blog here if that matters.

Apparently I can test my unit tests easier right? Would I only test the viewModel in MVVM? What needs to be tested primarily ?

like image 304
j2emanue Avatar asked Feb 06 '16 04:02

j2emanue


People also ask

Can we use MVVM without databinding?

You can still use MVVM without data binding but you need some way to notify the view about data changes, It can be LiveData (preferred way), Java Observable, Rx or even a custom implementation.

What is data binding in MVVM Android?

Up until now, we've used Data Binding to update the View from the ViewModel. LiveData is a handy data holder that acts as a container over the data to be passed. The best thing about LiveData is that it is lifecycle aware. So if you are in the background, the UI won't try to update.

What is MVVM pattern in Android?

MVVM stands for Model, View, ViewModel. Model: This holds the data of the application. It cannot directly talk to the View. Generally, it's recommended to expose the data to the ViewModel through Observables. View: It represents the UI of the application devoid of any Application Logic.

What is two way binding in MVVM?

Two-way data binding is nothing but updating the data source if there are any changes in the layout and vice versa. Two-way data binding is not applicable for all the views in Android. For example, using two-way data binding in EditText makes sense because users can update the data in the view.


1 Answers

You are correct with your assumption that you only unit test the ViewModel and the Model. The UI itself is not tested via unit tests, though you can also do automated UI tests, which is different from a unit test.

As of now, your example class itself isn't very unit test friendly. One of the main things MVVM is trying to accomplish (other than separation of concerns) is to decouple your code. Your ViewModel should only contain presentation logic, but no business logic. This goes into the Model layer of MVVM.

Your ViewModel is tightly coupled, because you are instantiating MyCountDownTimer inside your ViewModel. Because of that, you can't do a unit test anymore as every time you test the ViewModel class, you will also test MyCountDownTimer. This turns your unit test into an integration test (testing multiple components working together).

A unit tests per definition should only test a very specific type/class or a certain code block. In other words a unit of code, hence the name: unit test. To achieve this, you need to decouple the dependencies of your object you want to test.

You decouple your objects by splitting them into an interface and an implementation, then inject the the concrete implementation into your object, usually via constructor injection.

For example:

public class ViewModel extends BaseObservable {
    private long countDownTime;
    // Use final keyword here, so mCountDownTimer can only be set in the constructor and never changed
    // this enforces the the classes invariants and once initialized, you'll be sure
    // that it never can be null, so no need to do null checks before using
    private final MyCountDownTimerInterface mCountDownTimer;
    private final String TAG = getClass().getSimpleName();

    public ViewModel(MyCountDownTimerInterface mCountDownTimer) {
        if(countDownTimer == null) {
            throw new IllegalArgumentException("countDownTimer can't be null. ");
        }

        this.mCountDownTimer = countDownTimer;
    }

    @Bindable
    public long getCountDownTime() {
        return countDownTime;
    }


    public void setCountDownTime(long countDownTime) {
        this.countDownTime = countDownTime;

        notifyPropertyChanged((int) BR.countDownTime);
    }

    public void startCounting(Long milli) {
        this.mCountDownTimer.restartTimer(milli);
    }
}

Now, you can test your ViewModel without a concrete instance of your MyCountDownTimer class.

Since your example only contains behavior and no result testing, you will have to do a behavior test in your, like

  • If I call startCounting(10L), then restartTimer(10L) must be called in MyCountDownTimerInterface and getCountDownTime() must return 10L.

In order to do this, you would have to mock the MyCountDownTimerInterface interface and pass the mocked object inside. Mocks can be setup to verify that a certain method of the mocked interface is called with a certain parameter.

I can't offer you any code for this, as I am not familiar with the Java/Android Mock frameworks. I'm a C#/.NET developer. But if you don't know how to mock an interface for a behavior driven unit test, ask a new question here on StackOverflow :)

like image 162
Tseng Avatar answered Oct 15 '22 02:10

Tseng