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

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