Local testing Spring + GCP Firestore

Claudio Rauso
6 min readJun 3, 2023

--

As a developer of a Spring application that uses Firestore database, you may want to test the code on your machine without having to deal with an actual GCP project.

This post wants to be an end-to-end overview of this testing scenario, aimed at Java developers already familiar with Firestore basic concepts: all informations reported are available on various official Google resources, and have been put together as a result of hands-on experience.

A GitHub repository with sample code is available here.

All the instructions given are not intended for production usage or deployment in a shared environment, only for local testing.

This post comprises two parts:

  1. setting up a local emulator and creating a document
  2. configuring emulator connection in a Java Spring application and testing it

Part 1 can also be useful if you have a client application written in another programming language that can likewise be connected to the emulator.

Part 1: dealing with the emulator

We have two options to emulate Firestore on our machine: Firebase Local Emulator Suite and Firestore emulator.

The former is a complete set of emulators for Firebase services:

The Firebase Local Emulator Suite is a set of advanced tools for developers looking to build and test apps locally using Cloud Firestore, Realtime Database, Cloud Storage for Firebase, Authentication, Firebase Hosting, Cloud Functions (beta), Pub/Sub (beta), and Firebase Extensions (beta). It provides a rich user interface to help you get running and prototyping quickly.

Instead, Firestore emulator is a single-purpose tool that relies on Google Cloud CLI:

The Google Cloud CLI provides a local, in-memory emulator for Firestore that you can use to test your application. You can use the emulator with all Firestore client libraries.

Since we are developing something that connects to Firestore, it is likely that we also use (or will do in the future) other GCP/Firebase services, so it could come handy to have Emulator Suite installed: considering this, we take Firebase Local Emulator Suite as reference.

Installing Firebase Local Emulator Suite

For a complete and up-to-date installation guide please refer to Firebase docs. Briefly, we need to install Node.js, a JDK and Firebase CLI.

Once installed Firebase CLI, we can initialize the desired emulators. Firebase docs also explain details of this process.

This operation creates some configuration files in the working directory, so make sure to execute it in a proper folder (a freshly created one is a good choice). The command to perform emulators initialization is the following:

firebase init emulators

This will start some prompts:

  1. Default project — since we will not use an existing project, we select “Don’t set up a default project”
  2. Emulators setup — as seen before, many services can be tested using Local Emulator Suite: select “Firestore Emulator”
  3. Port selection — port that will be used to connect to the emulator: the default is 8080 but this could be a problem for us since we are developing a Spring Boot application (it has the same default port), so we choose 9080 port instead
  4. Emulator UI settings — whether or not to enable the Emulator UI and its port: we can keep the default values
  5. Emulators download — whether or not to immediately download the additional binaries for the selected emulators

You can find an example of these steps in the video below.

Example of Firebase Local Emulator Suite initialization

Starting the emulator

Once installed and set up Local Emulator Suite, we can start the selected emulators using the emulators:start command. For the entire list of startup parameters, see Firebase docs.

Since we didn’t choose a default project, we must pass the --project parameter, that must be set with a formally valid GCP project ID: this project can be an existing one or not (it will only be used by the emulator, not for connection to Google Cloud): from now on we will use a dummy project name.

To persist data across testing sessions we also use two parameters: --export-on-exit and --import, using the same value for both. These parameters, as the names suggests, tell the emulator where to dump data when stopping, and from where to import documents at startup. The value of these parameters must be a valid path, relative or absolute. In our example we use a relative path, so the data will be written in a subfolder of the one where we have initialized our emulator.

firebase emulators:start --project local-project --export-on-exit=data --import=data

The output of the start command presents a list of started emulators with their host and port: these values, together with the project ID, will be used to configure the Spring application.

Example of output for emulator start command
Example of output for start command

Creating your first document

Now that the emulator has been started we can connect our application to the database, but we can also interact with it using the Emulator UI.

From the Emulator UI it is possible to create, update and delete collections and documents. Find an example below of how to create the first collection adding a document to it. We create the users collection later used in the code examples.

UI for Firebase Local Emulator Suite (Firestore tab)

Part 2: testing the Spring application

Full source code of a simple test application can be found in the linked GitHub repository: it is a Maven project with a Spring Boot application using some well known Spring/Spring Cloud modules.

Let’s review some key aspects.

Dependencies

We need spring-cloud-gcp-starter-data-firestore to get auto-configuration and Spring Data capabilities for Firestore.

<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-starter-data-firestore</artifactId>
</dependency>
</dependencies>

Configuration: application properties

The Spring Data Firestore starter has three fundamental properties for our purpose:

  • emulator.enabled boolean flag to activate emulator connection
  • host-port hostname and port of the Firestore emulator as seen at emulator startup (<host>:<port> format)
  • project-id project ID, to be set with the one used in the emulator
# spring properties
spring:
cloud:
gcp:
firestore:
emulator:
enabled: true
host-port: localhost:9080
project-id: local-project

Code: entity and repository

Since we are using the Spring starter, no additional configuration is needed: all we have to do is taking care of our implementation.

To read and write data, the first thing to do is to declare the model we want to use: a simple Java class annotated with @Document is all we need. In order to access the document ID (that can be auto generated by Firestore or explicitly set in our code) we also must put the @DocumentId annotation on a String field.

import com.google.cloud.firestore.annotation.DocumentId;
import com.google.cloud.spring.data.firestore.Document;

@Document(collectionName = "users")
public class UserDocument {

@DocumentId private String id;

private String name;

private String surname;

public UserDocument() {
super();
}

public UserDocument(String name, String surname) {
this.name = name;
this.surname = surname;
}

public String getId() {
return id;
}

public String getName() {
return name;
}

public String getSurname() {
return surname;
}
}

Note that the @Document annotation takes an (optional) parameter to declare collection name: Spring Data will use that value to compose the path of our document. If not set, class name will be used as collection name.

To complete our data access layer we now have to define a repository for our entity. Following Spring Data conventions, all we have to do is to declare an interface extending one of the interfaces provided by the framework. Since we are using the Firestore module, FirestoreReactiveRepository is the one to go.

@Repository
public interface UserRepository extends FirestoreReactiveRepository<UserDocument> {}

FirestoreReactiveRepository interface is based on Project Reactor, so all generated methods will be non-blocking operations returning Mono and Flux publishers. However, blocking calls are also possible adding an invocation to the .block() method in the code that will use the repository, as shown in the example below.

@Service
public class UserService {

private final UserRepository repository;

public UserService(UserRepository repository) {
this.repository = repository;
}

public User get(String id) {

if (!StringUtils.hasText(id)) {
throw new InvalidInputException();
}

return repository
.findById(id)
.map(doc -> new User(doc.getId(), doc.getName(), doc.getSurname()))
.block();
}

public User add(User user) {

UserDocument doc =
Optional.ofNullable(user)
.map(u -> new UserDocument(u.getName(), u.getSurname()))
.orElseThrow(InvalidInputException::new);

final var id = Objects.requireNonNull(repository.save(doc).block()).getId();
user.setId(id);

return user;
}
}

Read and write data

In the example application that can be found here, there is also a controller with two endpoints (simply invoking methods from the UserService class shown before):

  • GET /demo/users/{userId} to read a specific user data by id
  • POST /demo/users to add a new user

When posting a new user you can then retrieve the created data using the GET endpoint, and the documents will also be visible in the Emulator UI. As stated before, starting the emulator with export-on-exit and import flags, any data will be persisted and available even if we restart the emulator.

Find an example of write and read operations in the video below.

Testing read and write operations

That’s all. Feel free to share your thoughts or requests for clarification.

Thanks.

--

--

Claudio Rauso

Software Engineer @ Generali jeniot. Certified Google Cloud PCA and Scrum Master.