Vehicle Location using SpringBoot 3 with gRPC, OpenTelemetry and Micrometer Tracing — Part 3

Gabriel Martins
4 min readNov 21, 2023

This is the final part of this article and we will focus on testing.

We can create unit tests for server using JUnit e other resources like this example

import br.gasmartins.grpc.sensors.SensorData;
import br.gasmartins.grpc.sensors.SensorDataPage;
import br.gasmartins.grpc.sensors.SensorServiceGrpc;
import br.gasmartins.sensors.interfaces.grpc.advice.GrpcExceptionControllerAdvice;
import br.gasmartins.sensors.application.service.SensorService;
import com.google.protobuf.StringValue;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;
import lombok.Getter;
import net.devh.boot.grpc.server.autoconfigure.GrpcAdviceAutoConfiguration;
import net.devh.boot.grpc.server.autoconfigure.GrpcReflectionServiceAutoConfiguration;
import net.devh.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration;
import net.devh.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import static br.gasmartins.sensors.interfaces.grpc.support.SensorDataDtoSupport.*;
import static br.gasmartins.sensors.domain.support.SensorDataSupport.defaultSensorData;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@ExtendWith(SpringExtension.class)
@Import({SensorGrpcController.class, GrpcExceptionControllerAdvice.class})
@ImportAutoConfiguration({
GrpcReflectionServiceAutoConfiguration.class,
GrpcAdviceAutoConfiguration.class,
GrpcServerAutoConfiguration.class,
GrpcServerFactoryAutoConfiguration.class
})
@ActiveProfiles("test")
@ContextConfiguration(initializers = ConfigDataApplicationContextInitializer.class)
@DirtiesContext
class SensorGrpcControllerTest {

@MockBean
private SensorService service;

private ManagedChannel channel;
private SensorServiceGrpc.SensorServiceBlockingStub blockingStub;
private SensorServiceGrpc.SensorServiceStub stub;

@BeforeEach
public void setup() {
this.channel = ManagedChannelBuilder.forAddress("localhost", 8087)
.usePlaintext()
.build();
this.blockingStub = SensorServiceGrpc.newBlockingStub(this.channel);
this.stub = SensorServiceGrpc.newStub(this.channel);
}

@AfterEach
public void tearDown() {
this.channel.shutdown();
}

@Test
@DisplayName("Given Sensor Data When Store Then Return Stored Sensor Data")
public void givenSensorDataWhenStoreThenReturnStoredSensorData() {
var responseObserver = new StreamObserver<SensorData>() {

@Getter
private final List<SensorData> data = new ArrayList<>();
@Override
public void onNext(SensorData value) {
this.data.add(value);
}

@Override
public void onError(Throwable t) {
fail("Error while processing request");
}

@Override
public void onCompleted() {
System.out.println("Response is complete");
}
};

when(this.service.store(any(br.gasmartins.sensors.domain.SensorData.class))).thenAnswer(invocation -> invocation.getArgument(0));

var requestObserver = stub.store(responseObserver);

var sensorDataDto = defaultSensorDataDto().build();
requestObserver.onNext(sensorDataDto);
requestObserver.onCompleted();

await().atMost(30, TimeUnit.SECONDS)
.untilAsserted(() -> assertThat(responseObserver.getData()).isNotNull());
}

@Test
@DisplayName("Given Sensor Id When Not Exists Then Throw Exception")
public void givenSensorIdWhenNotExistsThenThrowException() {
var sensorData = defaultSensorData().build();
var id = UUID.randomUUID();
when(this.service.findById(id)).thenReturn(sensorData);

var request = StringValue.newBuilder()
.setValue(id.toString())
.build();
var existingSensorData = this.blockingStub.findBySensorId(request);
assertThat(existingSensorData).isNotNull();
}

@Test
@DisplayName("Given Vehicle Id And Interval When Exists Then Return Sensor Data Page")
public void givenVehicleIdAndIntervalWhenExistsThenReturnSensorDataPage() {
var responseObserver = new StreamObserver<SensorDataPage>() {

@Getter
private final List<SensorDataPage> data = new ArrayList<>();

@Override
public void onNext(SensorDataPage value) {
this.data.add(value);
}

@Override
public void onError(Throwable t) {
fail("Error while processing request");
}

@Override
public void onCompleted() {
System.out.println("Response is complete");
}
};

var requestObserver = this.stub.findByVehicleIdAndOccurredOnBetween(responseObserver);
var paramDto = defaultSearchSensorDataByVehicleIdParamDto().build();
requestObserver.onNext(paramDto);
requestObserver.onCompleted();

await().atMost(30, TimeUnit.SECONDS)
.untilAsserted(() -> assertThat(responseObserver.getData()).isNotEmpty());
}

}

And for client test we can do this

import br.gasmartins.sensors.infra.grpc.support.LocationGrpcServiceMock;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.io.IOException;

import static br.gasmartins.sensors.domain.support.CoordinatesSupport.defaultCoordinates;
import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@ActiveProfiles("test")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@DirtiesContext
class LocationGrpcAdapterTest {

private final LocationGrpcAdapter adapter;
private Server server;

@BeforeEach
public void setup() throws IOException {
this.server = ServerBuilder.forPort(8085)
.addService(new LocationGrpcServiceMock())
.build();
server.start();
var serverThread = new Thread(() -> {
try {
server.awaitTermination();
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
});
serverThread.setDaemon(false);
serverThread.start();
}

@AfterEach
public void afterEach() {
this.server.shutdownNow();
}

@Test
@DisplayName("Given Coordinates When Exists Then Return Location")
public void givenCoordinatesWhenExistsThenReturnLocation() {
var coordinates = defaultCoordinates().build();

var location = this.adapter.findByCoordinates(coordinates);

assertThat(location).isNotNull();
}

}

The @SpringBootTest annotation was necessary because the definion the the client bean

import br.gasmartins.grpc.locations.LocationServiceGrpc;
import br.gasmartins.sensors.infra.grpc.LocationGrpcAdapter;
import net.devh.boot.grpc.client.inject.GrpcClient;
import net.devh.boot.grpc.client.inject.GrpcClientBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@GrpcClientBean(
clazz = LocationServiceGrpc.LocationServiceBlockingStub.class,
beanName = "locationClientBlockingStub",
client = @GrpcClient("location-client")
)
public class LocationGrpcClientConfiguration {

@Bean
public LocationGrpcAdapter locationGrpcAdapter(LocationServiceGrpc.LocationServiceBlockingStub stub) {
return new LocationGrpcAdapter(stub);
}

}

If you are not using constructor to inject the client and are just using the @GrpcClient annotation you can replace the @SpringBootTest annotation with this

@ImportAutoConfiguration({
GrpcReflectionServiceAutoConfiguration.class,
GrpcAdviceAutoConfiguration.class,
GrpcServerAutoConfiguration.class,
GrpcServerFactoryAutoConfiguration.class,
GrpcClientAutoConfiguration.class
})

Now to test the routes I strongly recommend the Postman tool.

In Postman go to the menu New > gRPC > Service Definition > Import .proto file > Add an import path

Choose the path <path_to_the_folder>\vehicle-location-grpc-tracing\sensor-service\src\main\proto

After that click in Choose a file, select the proto file and click on Import button

Use the endpoint localhost:8087 and select the desired method

You can check the traces on Jaeger using http://localhost:16686/

And about the Grafana’s dashboard you can use this url http://localhost:3000/d/vehicle_location_system_monitor/vehicle-location-system-monitor

The traces on grafana tempo can be visualized on grafana too

You can see more details on Github Project

References

https://spring.io/blog/2022/10/12/observability-with-spring-boot-3

Practical Domain-Driven Design in Enterprise Java: Using Jakarta EE, Eclipse MicroProfile, Spring Boot, and the Axon Framework by Vijay Nair

--

--

Gabriel Martins

Just a brazilian guy who loves technology and wants to share the experiences he has learned