Test First Then Refactor

Chuck Greb
Sep 6, 2018 · 3 min read

Tests are the safety net that allow you to refactor with confidence. But what if your legacy code doesn’t have any tests? Then add some!

Ordinarily, I would advocate for a unit test first strategy on Android. Instrumentation tests and UI tests can come after your logic is well tested.

However, when dealing with untested legacy code, sometimes we need to talk a different approach. Let’s look at a simple example.

MainActivity.kt

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

toggle_button.setOnClickListener {
if
(image_view.visibility == View.VISIBLE) {
image_view.visibility = View.GONE
} else {
image_view.visibility = View.VISIBLE
}
}
}
}

In this app we have a Button and an ImageView. When the user clicks the button, it toggles the visibility of the image.

The issue here is that our presentation logic is mixed into with our Activity. This makes it challenging to maintain, hard to modify, and hard to test. By definition it is legacy code.

Let’s say we want to make a change to the presentation logic. For example, once the view is shown do not hide it again. Once we implement the change, our only option is to launch the app on a device and manually test it.

We know there are a lot of new features planned for this part of the app. In order to make the code more flexible and maintainable, we decide to migrate the presentation logic to a presenter class.

But wait! How can we be sure that when we introduce the presenter class, we don’t also accidentally change the behavior of the code? First, we need to write some tests.

Unfortunately, since the presentation logic is currently stuffed into the Activity there is no way we an unit test it. We need to add some instrumentation tests using Espresso.

MainActivityTest.kt

@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@get:Rule
val rule: ActivityTestRule<MainActivity>
= ActivityTestRule(MainActivity::class.java)

@Test
fun onClick_once_shouldHideImageView() {
onView(withId(R.id.toggle_button)).perform(click())
onView(withId(R.id.image_view))
.check(matches(not(isDisplayed())))
}

@Test
fun onClick_twice_shouldShowImageView() {
onView(withId(R.id.toggle_button)).perform(click())
onView(withId(R.id.toggle_button)).perform(click())
onView(withId(R.id.image_view))
.check(matches(isDisplayed()))
}
}

We add one test for the scenario when the image is currently visible, and the other for when the image is currently hidden. Now, we can begin to refactor.

First, we add the presenter class.

MainPresenter.kt

class MainPresenter(private val controller: MainController) {
fun onToggleButtonClick(visibility: Int) {
if (visibility == View.VISIBLE) {
controller.setImageVisibility(View.GONE)
} else {
controller.setImageVisibility(View.VISIBLE)
}
}

Next, we add the controller interface the presenter will use to communicate with the view layer.

MainController.kt

interface MainController {
fun setImageVisibility(visibility: Int)
}

Now we can refactor our MainActivity to (1) implement the controller interface and (2) delegate to the new presenter class to handle user input and presentation logic.

MainActivity.kt

class MainActivity : Activity(), MainController {
private val presenter = MainPresenter(this)

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

toggle_button.setOnClickListener {
presenter.onToggleButtonClick(image_view.visibility)
}
}

override fun setImageVisibility(visibility: Int) {
image_view.visibility = visibility
}
}

Last, but definitely not least, we can now write some pure unit tests for our presentation logic.

MainPresenterTest.kt

class MainPresenterTest {
private val controller = TestController()
private val presenter = MainPresenter(controller)

@Test
fun onToggleButtonClick_visible_shouldSetImageGone() {
controller.visibility = View.GONE
presenter.onToggleButtonClick(View.VISIBLE)
assertEquals(View.GONE, controller.visibility)
}

@Test
fun onToggleButtonClick_gone_shouldSetImageVisible() {
controller.visibility = View.VISIBLE
presenter.onToggleButtonClick(View.GONE)
assertEquals(View.VISIBLE, controller.visibility)
}

class TestController : MainController {
var visibility = View.VISIBLE

override fun setImageVisibility(visibility: Int) {
this.visibility = visibility
}
}
}

Now the code is in a much better position to make future changes. We have unit tests, instrumentation tests, and clear separation of concerns. But most importantly, we have confidence that our refactor improved the design without changing the behavior of the production code.

This post is part of a series on working with legacy code on Android. It explores ways we can navigate, maintain, improve, and evolve legacy code using clean architecture, refactoring, dependency breaking techniques, and testing.

If you found this article helpful, please give it some applause 👏 to help others find it. You can also leave a comment below. Thanks!

Android Testing

Stories and advice to help you write better software and tests on Android

Chuck Greb

Written by

Open source aficionado, test-driven evangelist, and clean code connoisseur. Mostly Android. Building @ Button.

Android Testing

Stories and advice to help you write better software and tests on Android

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade