6

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 ?

Tseng
  • 61,549
  • 15
  • 193
  • 205
j2emanue
  • 60,549
  • 65
  • 286
  • 456

1 Answers1

5

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 :)

Tseng
  • 61,549
  • 15
  • 193
  • 205
  • Hey appreciate the feedback indeed. What your testing is CountDownTimerInterface. Makes sense. But in my case what would the Model be ? – j2emanue Feb 06 '16 at 18:24
  • 1
    No, I am not testing `CountDownTimerInterface`. It is mocked. I am testing the ViewModel's behavior (as in this example there is no data to be tested). In your example you use `MyCountDownTimer` and it's impossible to test your `ViewModel` in isolation, because whenever you call `this.mCountDownTimer.something` you will be testing the counter, and not your view model. I removed this coupling, by replacing the concrete counter implementation with an interface and this interface will be mocked during the test (not using the real `MyCountDownTimer` class) – Tseng Feb 06 '16 at 19:06
  • 1
    @j2emanue: Here a link to behavior driven design (BDD) https://en.wikipedia.org/wiki/Behavior-driven_development – Tseng Feb 06 '16 at 19:08