Our approach to Testing with Android
In this blog post, I will introduce you to the strategies we defined to thoroughly test the Gousto Android App and go into detail about the different approaches we use for testing the different layers of our app.
I will start by giving you an overview of our architecture.
Adapted to Android and to suit our needs, we decided upon an architecture that looks like the following diagram:
Our architecture is divided in 3 layers: Presentation, Domain and Data.
- Data: holds any sort of processing and mapping of raw data coming from any internal sources (shared preferences, Database) and external sources (Gousto API)
- Domain: the business logic divided into use cases
- Presentation: the UI (activities, fragment, views) and UI scenarios logic (presenters)
Testing Coverage Approach
In this section, I’ll describe our approach to testing different parts of the architecture. I’ll follow the framework goal - approach - coverage. The goal is what we are trying to achieve by testing a specific part of the architecture, the approach is how we decided to achieve the goal, and coverage is a brief overview of the components covered.
This layer is mainly made of data source implementation and manipulation of data.
- Give us the right confidence in the authenticity of the calls/queries we’re making.
- Give us the assurance the tests will fail if the data structure changes, isn’t processed in the same way or just can’t be manipulated anymore (after a refactor for example or a minor change in a request body)
- Play a key role in describing our data manipulation rules.
- Therefore we need a full coverage of regression tests only.
- Pure java unit tests running on the JVM for most of the suite and isolated robolectric unit tests for date source implementations based on Android framework components (SharedPreferences, SQLiteOpenHelper)
- We mock the data source implementations when testing the repositories.
- every network request
- every database query
- tiers modules (custom json serializer & deserializer, …)
This layer is made of use cases representing the business logic of the app
- Give us confidence in the robustness of our business logic
- List and Describe the app functionalities and its business requirements
- We use functional test with different inputs in order to test the business logic in different conditions.
- Pure java unit test running on the JVM
- We mock only the repositories.
- every use case
- every mapper
Presenters are components of the presentation layer that follow a specific scenario with specific command. It collects & maps the data in an easy to display & logic-free view-model and triggers the UI to render it.
- Describe UI behaviour and list every possible scenarios.
- Functional tests asserting presentation behaviour for different flows/states & commands.
- Pure java unit tests running on the JVM
- Mocking of the use cases and mappers
- every presenter
This is the end part of the presentation layer. It consists of any component visible to the customer + ui logic (ex: animation)
- Pure UI tests asserting only of the view changes
- UI tests will run on a device with AndroidJunit runner. We mock the presetenters serving custom view-models
- View components
End-to-end UI tests covering critical flows (Signup, Order Checkout, …). Only the network client is mocked and we control the state of the app with dummy JSON responses.
Bugs & Crashes
Unit tests are one of the way we tackle issues & crashes.
TDD is the recommended way but what really matters is to have a test covering the crash to make sure it doesn’t happen again (and also release a fix for it).
This approach is building up the robustness of the app overtime (and confidence!)
Unit tests are also an excellent way to deal with legacy code.
There are usually 2 cases: already unit tested or not unit tested.
In the presence of tests, we would just update the implementation based on the unit test expectations & assertions.
In the absence of tests, we would either write the unit tests for the legacy code first based on our comprehension of the implementation, then refactor the legacy code. The Second approach would be to not touch the legacy code at all but to rewrite the code from scratch first with a TDD approach, then just replace the call, write a functional test and remove the legacy code.
- Espresso: UI testing framework
- Junit: Run unit tests on JVM instead of a device
- AssertJ: to have nicer assertion, easily debuggable and to be more accurate on the expected behaviour of an assertion
- Mockito: mocking & stubbing
- Jacoco: to measure our test coverage
- to test implementations using exclusively android APIs (ex: SharedPreferences)
- to test legacy code too tight to the android framework
Android Software Engineer