5

I have a big project for Android with Kotlin using the MVP pattern, and I'm starting to struggle to do Unit Tests (testing the presenters mocking the view interfaces). The reason is I'm passing view references to my functions in the presenters and it's really bad having to mock them, example:

My code would look like:

class MainActivity : Activity(), MainActivityView {

    @BindView(R.id.numberTV)
    lateinit var numberTV : AppCompatTextView

    private val mainActivityPresenter = MainActivityPresenter(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mainActivityPresenter.onCreate()
    }

    override fun showNumber() {
        mainActivityPresenter.showNumber(numberTV, 22)
    }

}

interface MainActivityView {
    fun showNumber()
}

class MainActivityPresenter(private val mainActivityView: MainActivityView) {
    fun showNumber(numberTV: AppCompatTextView, number: Int) {
        numberTV.text = if (number < 0) {
            "Not compatible"
        } else if (number < 10) {
            number.toString()
        } else {
            "9+"
        }
    }

    fun onCreate() {
        mainActivityView.showNumber()
    }
}

My current problem is, when I'm testing the function showNumber(AppCompatTextView, Int) with Mockito in Unit Tests, I should mock the view just to pass the test (as it can't be null).

Which one would be a better approach to do Unit Tests here?

My thoughts are:

  1. Grabbing the numberTV: AppCompatTextView from the MainActivityPresenter, such as mainActivityPresenter.getBindViews().mainIV
  2. Returning just the text logic ("Not compatible", number.toString() or "9+") from the presenter, although sometimes the requirements require to do logic between more than view (2+)

What would you do?


EDIT

I would like to point out that not passing any View to the presenter, might be an issue with asynchronous calls.

Example: setting an image with Glide:

internal fun showImageAccordingToCache(cachedSplashScreenUri: String?, mainImageView: ImageView) {
    Glide.with(context)
            /**
             * Save in cache, but we say to load it from cache. Otherwise it will throw an error
             */
            .setDefaultRequestOptions(defaultDiskStrategy()
                    .onlyRetrieveFromCache(true))
            .load(cachedSplashScreenUri)
            .listener(object : RequestListener<Drawable> {
                override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
                    /**
                     * And when that error is thrown, we preload the image for the next time.
                     */
                    activityViewPresenter.showLogo()
                    loadImageInCache(cachedSplashScreenUri)
                    return true
                }

                override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
                    return false
                }
            })
            .into(mainImageView)
}
Rafael Ruiz Muñoz
  • 5,333
  • 6
  • 46
  • 92

2 Answers2

1

I think you should not pass views to your presenter. Your presenter should call mainActivityView methods to show the required Data. This should be the method in your mainAcitivityView

override fun showNumber(number: String) {
    numberTV.text = number
}

And you should call this from your presenter like this

fun onCreate() {
   showNumber(22)
}

fun showNumber(number: Int) {
    numberString:String = if (number < 0) {
        "Not compatible"
    } else if (number < 10) {
        number.toString()
    } else {
        "9+"
    }
    mainActivityView.showNumber(numberString:String)
}
Suhaib Roomy
  • 2,501
  • 1
  • 16
  • 22
  • What about with asynchronous calls? I'm adding one example to my question now. – Rafael Ruiz Muñoz May 25 '18 at 12:45
  • For this particular user you will have use a mock view i testing, It would be better that presenter request view rather than mainActivityView passing it to present, like ".into(mainActivityView.getImageView)" – Suhaib Roomy May 25 '18 at 15:44
1

It's normal to have an interface View in your presenter. In your case MainActivityView is the interface that represents the contract your Activity must comply to. You'd normally pass that view in the constructor of the presenter (what you're already doing) on by using dagger to inject it into the presenter.

Now this, is not very usual:

 fun showNumber(numberTV: AppCompatTextView, number: Int) {
        numberTV.text = if (number < 0) {
            "Not compatible"
        } else if (number < 10) {
            number.toString()
        } else {
            "9+"
        }
    }

Now the presenter knows about "Android SDK components" which is not a good thing. In this case what you should be doing is this:

 fun showNumber(number: Int) {
        if (number < 0) {
            mainActivityView.setNumberText("Not compatible");
        } else if (number < 10) {
            mainActivityView.setNumberText(number.toString());
        } else {
            mainActivityView.setNumberText("9+");
        }
    }

To test this you would mock the view and see if depending on number each of those methods were actually called.(In java).

@Mock
MainActivityView view;

@Test
public fun shouldShowCorrectNumber() {

    int number = 10;
    presenter.showNumber(number);
    verify(view).showNumber("9+");
}

As for async calls, what I usually see when using the Glide library is that it is used in the activity, not in the presenter. Other types of async calls might go in other layers, for example. I usually see network calls in the Interactor layer with callbacks to the presenter:

Levi Moreira
  • 11,917
  • 4
  • 32
  • 46
  • Bravo!! What would you say in case we're using a view asynchronously? If you can provide some valid discussion of it, I'll mark it as correct answer because that's my last concern! – Rafael Ruiz Muñoz May 25 '18 at 13:14
  • Thanks!! will start re-architecturing everything as I've been passing Views (as reference) to the presenters all the time. – Rafael Ruiz Muñoz May 25 '18 at 13:16
  • 1
    Glad I could help :) When your presenter starts needing certain Android SDK components, that's when you should check the architecture, because it usually doesn't need them – Levi Moreira May 25 '18 at 13:18