This is the first post in a series where we’ll attempt to safely and gradually refactor a legacy app written in Flutter.

If you don’t know what Flutter is, check out the awesome 2-part episode of the Fragmented podcast or this Google I/O presentation.

If you do know what Flutter is and are wondering: “how can there be any legacy code in Flutter? It’s not old enough for that”. The answer is in Michael Feather’s definition. He defends the idea that legacy code is not necessarily old code, but code without automated tests.

The Goal

Here’s what we’ll do: we will cover the app with automated tests, so that we have a safety net in case we make mistakes, and then we’ll refactor the code and apply the MVP architectural pattern.

The App

The app we’re going to refactor is the beautiful Planets app created by Sergi Martínez. This app is nice because it looks great, Sergi has a whole series showing how he built it and he focused solely on the UI part.

This means we have a cool, working app with lots of room for improvement in terms of architecture. If you were building a prototype and had to move fast (like in a startup) this is probably how you would’ve done it.

You can see this series as the natural next step. You’ve built a prototype and validated your idea. Now it’s time to make it production-ready.

The Code

To get started, checkout the code from the Github repo. And if you want to jump straight to the solution, take a look at the part-1 branch.

Unit Tests vs Widget Tests

According to the Test Pyramid, we should have our applications covered by all types of automated tests, and unit tests should comprise the largest part of our test suite.

The issue that often arises when creating unit tests for legacy apps is that the code is usually not testable. This makes writing unit tests very hard and sometimes even impossible.

In order to write unit tests, we might have to change the code. But if we modify the code without tests in place, we take the risk of making mistakes and introducing bugs. (Read Michael Feather’s Working Effective with Legacy Code to learn more)

To overcome this problem, we’ll start with a higher level test. In Flutter world, this means widget tests. Once we have these tests in place, we can refactor the code to make it more unit testable.

Planet Summary

If you take your time to inspect the code, you’re gonna see that PlanetSummary is used in both pages of our app. In horizontal mode, it’s used as the rows of the HomePage . While in vertical mode, it’s used as the top section of the DetailPage .

Now let’s start to test. Create the file /test/ui/common/planet_summary_widget_test.dart with the content below.

This is what the code does:

Group is optional. It can be used to organize the code or, in our case, make it read nice. For instance, you can read “PlanetSummary shows a planet in horizontal mode”. We call testWidgets to create a widget test. tester.pumpWidget instantiates the widget we want to test. Note that we have to wrap it in a MaterialApp . We can now make assertions on our widget. As both of our tests perform the same checks, we can abstract it in a method to avoid repetition. Finally, we verify that PlanetSummary displays the desired information.

Now run the tests and …

Boom! The tests fail with the message ‘2 widgets with text “3.711 m/s”’. Hooray!!!! We’ve found a bug.

Let’s fix it by changing the following line in the PlanetSummary class.

Run the tests once more and all of them should pass.

Image Assertion

Our tests are looking nice. We even caught a bug. But right now we’re only verifying if the text is correct (through find.text ). What if we want to make sure we’re showing the right planet image?

To do so, create the file /test/util/finders.dart like this:

And now we can use this method in our test. Go ahead and change planet_summary_widget_test.dart :

Run the tests again and they still pass. Cool 😎

DetailPage

Now, let’s test the details page of our app. Create the file /test/ui/detail/detail_page_widget_test.dart and add this content to it.

Run this test and it will fail with the following error:

Exception: HTTP request failed, statusCode: 400,

https://www.nasa.gov/sites/default/files/thumbnails/image/pia21723-16.jpg

What’s going on here? It turns out the detail page tries to load the background image from the web. By default, any HTTP request sent in a widget test returns immediately with status code 400. As we’re using a NetworkImage to load the image, it throws an exception when it receives the 400 status code.

To fix this issue, download the mock_http_client.dart and add it to /test/util . After that, change detail_page_widget_test.dart :

And finally add mockito as a dependency in your pubspec.yaml :

Home Page

The final step in our quest is to test the home page. So go ahead and create the file /test/ui/home/home_page_widget_test.dart . Add this to the file:

Here’s what the code does:

Loop through the list of planets and verify that the first three planets are visible. Use tester.drag to scroll down “Wait” for the scroll action to end “Wait” for the navigation to happen Check that the detail page is displayed.

Run the tests once more and … voilà! Green tests. Our app is now all covered.

Bonus: Continuous Integration and Code Coverage

Now that we have all these widget tests, it’d be nice to run them often and know how much of the code they cover. That’s where Travis-CI and Coveralls come in. We can set up the former to build and run the tests on every commit and every night. And we can use the later to find the code coverage.

Thankfully, Yegor Jbanov and Marcin Szalek show this is easily achieved. Simply add the following .travis.yml to the root of your project, sign up to Travis-CI and Coveralls, and point them to your repository. It’s that simple.