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

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Gabriel Martins
Gabriel Martins

Written by Gabriel Martins

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

No responses yet

Write a response