This is an example that covers the basis for testing with Espresso a project using Firebase. There are some steps that can be achieved in a different manner, those are indicated. This project, covers specifically how to solve the Firebase problems and make it play along with Espresso.
In the following, you'll find how to set up the project and the step by step process.
The slides can be found in the root directory
This project is using androidx
if your project has not migrated yet the general approach should still be valid, you will need to change the reference accordingly.
In this momement there is an issue with Firebase dependencies and google-services:4.1.0
so please make sure you are using at least:
-
In the project gradle
classpath 'com.google.gms:google-services:4.2.0'
implementation 'com.firebaseui:firebase-ui-auth:4.3.2'
implementation 'com.google.firebase:firebase-core:16.0.8'
implementation 'com.google.firebase:firebase-database:16.1.0'
If you want to look at the final result you can switch to the workshop branch.
The project uses firebase-ui-auth for creating a quick login using email and then will move forward to another screen where a query to the RTD is performed and the UI is updated. The project us the MVP pattern.
This will guide you to run the basic app:
- Clone the project, fork it, however, you feel more comfortable
- You have to link the project with a Firebase project of your own, you can use the Android Studio Assistant or do it manually. The file google-services.json is not provided and for the public should be ignored from VCS.
- Make sure you have created the Real-Time Database for the Firebase project
- This are the database rules you need
{
"rules": {
"tasks": {
"$uid": {
".read": "auth.uid === $uid",
".write": "auth.uid === $uid"
}
}
}
}
- Run the app
- Create a user with the email
test@app.io
and the password12345678
(this is our test user) - After the login in the
MainActivity
, you should see 0 tasks - Now go the Firebase web console and in the Authentication section copy your user UID
- Replace the UID in the following data, create a JSON file with it, and upload it to the RTD
{
"tasks": {
"REPLACE_THIS_WITH_YOUR_UID": {
"-Lb4zAYdzn53BIsG4XcX": "one",
"-Lb4zAYdzn53BIsG4XcY": "two",
"-Lb4zAYdzn53BIsG4XcZ": "three"
}
}
}
- Restart the app, the user should be already logged and you will see 3 tasks.
The following is the step by step to build your Espresso tests, you can switch to a new branch or work on master, whatever suits you.
- Go to your
gradle app
and add the dependencies
android {
...
defaultConfig {
...
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
...
}
...
}
dependencies {
...
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
debugImplementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
}
This is mostly the same instructions that you could find in the Android Documentatio with a difference for our use case:
- We are adding the
espresso-idling-resource
as a debug dependency because we don't want to carry it to the release version of the app but still be able to use in our testing, we are gonna use build variants for this later (more info later as well)
When we move to create the tests, we will find 2 issues, the initializing the FirebaseApp and log in the user, we have partially take care of the second by creating a test user (test@app.io) in the sixth step of the setup. We now have to take care of what we need for initializing the FirebaseApp manually.
- In the root directory of the project create a file called
firebase.properties
if you are using the Android view you can change it to project or just do it using the explorer. You need to ignore that file from version control if you are planning to publish the repo - Open your
google-services.json
- This is the content of the
firebase.properties
file:
applicationId="1:REPLACE_THIS:android:NUMBERS_AND_LETTERS"
apiKey="AIzaREPLACE_THIS_SOME_RANDOM_CHARACTERS"
projectId="REPLACE_THIS_IS_THE_NAME_OF_YOUR_PROJECT"
databaseUrl="REPLACE_THIS_IS_THE_URL_OF_YOUR_PROJECT"
- You have to replace every text with the equivalent in the
google-services.json
file, the names are mostly the same - Now we have to make this credentials available for our project, we will do this using the
buildConfigField
in the module app gradle:
android {
...
buildTypes {
...
//You need to add this buildVariant it is always used for default, but now we needed explicitly
debug {
//This read the file we created
def fireFile = rootProject.file("firebase.properties")
def fireProperties = new Properties()
fireProperties.load(new FileInputStream(fireFile))
//This will get each key and make it available for the project
buildConfigField "String", "applicationId", fireProperties['applicationId']
buildConfigField "String", "apiKey", fireProperties['apiKey']
buildConfigField "String", "projectId", fireProperties['projectId']
buildConfigField "String", "databaseUrl", fireProperties['databaseUrl']
}
...
}
}
If you are confused here is the workshop file
As it was said, when testing a project using Firebase we will have to 2 problems:
- Initializing the FirebaseApp
- Login in a user
- Go to the
androidTest
directory, in this case, iscl.cutiko.espresofirebase (androidTest)
- Create an abstract class called
FireBaseTest
- Annotate your class with @RunWith(AndroidJUnit4.class) (this annotation is inherited so we can use it here)
- Implements
OnCompleteListener<AuthResult>
- This is how the base test should look
private static final String IDLING_NAME = "cl.cutiko.espresofirebase.FireBaseTest.key.IDLING_NAME";
private static final CountingIdlingResource idlingResource = new CountingIdlingResource(IDLING_NAME);
@Before
public void prepare() {
final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
int apps = FirebaseApp.getApps(context).size();
if (apps == 0) {
FirebaseOptions options = new FirebaseOptions.Builder()
.setApiKey(BuildConfig.apiKey)
.setApplicationId(BuildConfig.applicationId)
.setDatabaseUrl(BuildConfig.databaseUrl)
.setProjectId(BuildConfig.projectId)
.build();
FirebaseApp.initializeApp(context, options);
}
if (!new CurrentUser().isLogged()) {
IdlingRegistry.getInstance().register(idlingResource);
FirebaseAuth.getInstance()
.signInWithEmailAndPassword("test@app.io", "12345678")
.addOnCompleteListener(this);
idlingResource.increment();
onIdle();
}
}
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if (task.isSuccessful()) {
idlingResource.decrement();
} else {
fail("The user was not logged successfully");
}
}
- We are using the
@Before
annotation for making sure everything we need in our Firebase test to be ready before the tests. I don't recommend do this in the@BeforeClass
because that annotation forces the method to bestatic
which can turn inconvenient. - Since we are using the
@Before
annotation, this would happen every time before each@Test
method, that is why there are 2 validations, in case our tests files have more than 1 test. - The first we are doing is to make sure the
FirebaseApp
is not initialized previously. In our case it will be initialized, so why are we adding that code? Theapply plugin: 'com.google.gms.google-services'
can only be in the module app gradle, it takes care of reading thegoogle-services.json
file and initialize theFirebaseApp
, so no need for it as long as the project is a mono-module. Any multi-module project will have to solve this problem. As you can see the manual initialization is using the credentials we exposed as thebuildConfigField
on the gradle. - The second is to check if the user is logged or not if it is not logged, then we have to log, and only move forward when the loggin is completed. So if the user is not logged in, we sign in with the email and password we create for our test user.
This is the first time we are introducing the IdlingResource
, that is the Espresso library we add for debugImplementation
, it'll be good to take some lines to explain it further
You can find the official Android Documentation here, every sample for Espresso is here and this is the specific sample for IdlingResources
. Is strongly recommended to see the IdlingResourceSample.
IdlingResource works like a semaphore of work, Espresso use magic to know when there is work been done and has to wait and then when the work was completed to continue with the tests.
- Always register your IdlingResource
- Use the IdlingResource to notify there is work, Espresso will wait
- Use the IdlingResource to notify work is done, Espresso will continue the tests
- If there are no more test, make sure to use
onIdle
- It is recommended to unregister your IdlingResource
In this case, we are using a CountingIdlingResource
when the work started we increase it and when the work is done we decrease it. You can do your own IdlingResource by extending the Espresso classes, but for our purpose, this will work. The only warning is the CountingIdlingResource
can be corrupted if the decreasing happens more times than the increasing (lower than zero).
This could be done by other means, but this is the minimal approach:
CountDownLatch
having a framework to compare, is considered bad practice, take a look at this SO answer- Using RxFirebase, look at this SO answer
- This could be done using Kotlin Coroutines, you can take a look at this Google Lab
The @Before
method will be held until the login is successful or fails using the CountingIdlingResource
, and after that our tests will run, no race conditions.
The presenter is in charge of the workload, so one way to use the IdlingResource is to use it there, indicating when the work start and when the work finished. The problem is we don't need to have the IdlingResource on the release apk, remember we add the dependency as debugImplementation
at the start. So what we will do is to have a presenter for debug and another for release. The debug presenter will include the usage of IdlingResource while the release debug will not add any extra behavior. Our current presenter is on the main
directory, so we will have to refactor it to make it abstract
, then extend it on the build variant versions, and fix the reference in the MainActivity
.
- Open the
Presenter
refactor it to be calledMainPresenter
- Make it abstract
public abstract class MainPresenter implements MainContract.Presenter, ValueEventListener {
public MainPresenter(MainContract.View callback) {
this.callback = callback;
}
}
- Now we have to create the presenters for each build variant, if the Android Studio directory tree confuses you, change it to project or use the file explorer of the OS, your directory should look like the example:
espressofirebase/app/src
├── androidTest
|
├── debug
| └── java
| └── cl
| └── cutiko
| └── espresofirebase
| └── views
| └── main
| └── Presenter.java
|
├── main
| ├── java
| └── cl
| └── cutiko
| └── espresofirebase
| ├── data
| └── views
| ├── login
| └── main
| ├── MainActivity.java
| ├── MainContract.java
| └── MainPresenter.java
|
├── release
| └── java
| └── cl
| └── cutiko
| └── espresofirebase
| └── views
| └── main
| └── Presenter.java
|
└── test
- The
Presenter.java
in the release version will only extend theMainPresenter
public class Presenter extends MainPresenter
//Don't forget the mandatory constructor
- The
Presenter.java
in the debug version will extend theMainPresenter
and will use the IdlingResource. We are gonna make the IdlingResource easily accessible to make theIdlingRegistry
easier and we have to override the methods that start the work and end the work to add the IdlingResoruce correspondingly
public class Presenter extends MainPresenter {
private static final String MAIN_PRESENTER = "cl.cutiko.espresofirebase.views.main.Presenter.key.MAIN_PRESENTER";
public static final CountingIdlingResource idling = new CountingIdlingResource(MAIN_PRESENTER);
public Presenter(MainContract.View callback) {
super(callback);
}
@Override
public void getUserTasks() {
super.getUserTasks();
idling.increment();
}
@Override
public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
super.onDataChange(dataSnapshot);
idling.decrement();
}
@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
super.onCancelled(databaseError);
idling.decrement();
}
}
- Now go to the
MainActivity.class
and change the reference toPrensenter.java
public class MainActivity extends AppCompatActivity implements MainContract.View {
private TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = findViewById(R.id.taskCountTv);
//Here, fix this
new Presenter(this).getUserTasks();
}
//More code below
}
Now that we have all the parts we need to put them together. Go to the androidTest
and create the following class and methods
public class MainPresenterTest extends FireBaseTest implements MainContract.View {
private final Presenter presenter = new Presenter(this);
@Test
public void testGetUserTasks() {
IdlingRegistry.getInstance().register(Presenter.idling);
presenter.getUserTasks();
onIdle();
}
@Override
public void setTasksNumber(int count) {
assertEquals(3, count);
}
@Override
public void error() {
fail("Database Error");
}
}
- Always remember to register the IdlingResource, in this case, we can easily get it from the presenter
- The presenter inner work will use the IdlingResource to indicate when the work starts and end
- We have to use onIdle() method, after
presenter.getUserTasks()
there are no more Espresso test, so there we have to useonIdle()
otherwise it won't work - Remember we have 3 tasks in our RTD, however, the test could have been written differently
For creating the UI test of the MainActivity
is pretty much the same, we do have to be aware of some nuances. We can't use @Rule
annotation because the activity is starting the presenter on the onCreate
and @Rule
would happen before @Before
. So we have to launch the Activity in the test
public class MainActivityTest extends FireBaseTest {
@Test
public void taskCountTvTextTest() {
IdlingRegistry.getInstance().register(Presenter.idling);
ActivityScenario.launch(MainActivity.class);
final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
final Resources resources = context.getResources();
String text = resources .getQuantityString(R.plurals.tasks_plurals, 3, 3);
onView(withId(R.id.taskCountTv)).check(matches(withText(text)));
}
}
- Please notice that we don't need to use
onIdle()
this time because after the async work has started there are more Espresso tests, so Espresso is aware of the IdlingResource and knows it has to wait. - As usual, we have to register the IdlingResource again, we can easily obtain it from the
Presenter
For using the Firebase Test Lab select the other tab when launching the tests. You should know a couple of things:
- The configurations proposed there have plenty of unexistent devices, is better to create a device matrix directly on the web console
- The device Matrix created and your project can take some time to show up, don't know why usually one day is enough