Spring reactive : mixing RestTemplate & WebClient - spring-boot

I have two endpoints : /parent and /child/{parentId}
I need to return list of all Child
public class Parent {
private long id;
private Child child;
}
public class Child {
private long childId;
private String someAttribute;
}
However, call to /child/{parentId} is quite slow, so Im trying to do this:
Call /parent to get 100 parent data, using asynchronous RestTemplate
For each parent data, call /child/{parentId} to get detail
Add the result call to /child/{parentId} into resultList
When 100 calls to /child/{parentId} is done, return resultList
I use wrapper class since most endpoints returns JSON in format :
{
"next": "String",
"data": [
// parent goes here
]
}
So I wrap it in this
public class ResponseWrapper<T> {
private List<T> data;
private String next;
}
I wrote this code, but the resultList always return empty elements.
What is the correct way to achieve this?
public List<Child> getAllParents() {
var endpointParent = StringUtils.join(HOST, "/parent");
var resultList = new ArrayList<Child>();
var responseParent = restTemplate.exchange(endpointParent, HttpMethod.GET, httpEntity,
new ParameterizedTypeReference<ResponseWrapper<Parent>>() {
});
responseParent.getBody().getData().stream().forEach(parent -> {
var endpointChild = StringUtils.join(HOST, "/child/", parent.getId());
// async call due to slow endpoint child
webClient.get().uri(endpointChild).retrieve()
.bodyToMono(new ParameterizedTypeReference<ResponseWrapper<Child>>() {
}).map(wrapper -> wrapper.getData()).subscribe(children -> {
children.stream().forEach(child -> resultList.add(child));
});
});
return resultList;
}

Calling subscribe on a reactive type starts the processing but returns immediately; you have no guarantee at that point that the processing is done. So by the time your snippet is calling return resultList, the WebClient is probably is still busy fetching things.
You're better off discarding the async resttemplate (which is now deprecated in favour of WebClient) and build a single pipeline like:
public List<Child> getAllParents() {
var endpointParent = StringUtils.join(HOST, "/parent");
var resultList = new ArrayList<Child>();
Flux<Parent> parents = webClient.get().uri(endpointParent)
.retrieve().bodyToMono(ResponseWrapper.class)
.flatMapMany(wrapper -> Flux.fromIterable(wrapper.data));
return parents.flatMap(parent -> {
var endpointChild = StringUtils.join(HOST, "/child/", parent.getId());
return webClient.get().uri(endpointChild).retrieve()
.bodyToMono(new ParameterizedTypeReference<ResponseWrapper<Child>>() {
}).flatMapMany(wrapper -> Flux.fromIterable(wrapper.getData()));
}).collectList().block();
}
By default, the parents.flatMap operator will process elements with some concurrency (16 by default I believe). You can choose a different value by calling another variant of the Flux.flatMap operator with a chosen concurrency value.

Related

Spring unit tests [webflux, cloud]

I am new to the topic of unit testing and my question is whether I should perform the test as such of each line of code of a method or in what ways I can perform these tests to have a good coverage, if also, should exceptions be evaluated or not?
If for example I have this service method that also uses some helpers that communicate with other microservices, someone could give me examples of how to perform, thank you very much.
public Mono<BankAccountDto> save(BankAccountDto bankAccount) {
var count = findAccountsByCustomerId(bankAccount.getCustomerId()).count();
var customerDto = webClientCustomer
.findCustomerById(bankAccount.getCustomerId());
var accountType = bankAccount.getAccountType();
return customerDto
.zipWith(count)
.flatMap(tuple -> {
final CustomerDto custDto = tuple.getT1();
final long sizeAccounts = tuple.getT2();
final var customerType = custDto.getCustomerType();
if (webClientCustomer.isCustomerAuthorized(customerType, accountType, sizeAccounts)) {
return saveBankAccountAndRole(bankAccount);
}
return Mono.error(new Exception("....."));
});
}
EDIT
public Mono<BankAccountDto> save(BankAccountDto bankAccount) {
var count = findAccountsByCustomerId(bankAccount.getCustomerId()).count();
var customerDto = webClientCustomer
.findCustomerById(bankAccount.getCustomerId());
return customerDto
.zipWith(count)
.flatMap(tuple -> {
final var customDto = tuple.getT1();
final var sizeAccounts = tuple.getT2();
final var accountType = bankAccount.getAccountType();
// EDITED
return webClientCustomer.isCustomerAuthorized(customDto, accountType, sizeAccounts)
.flatMap(isAuthorized -> {
if (Boolean.TRUE.equals(isAuthorized)) {
return saveBankAccountAndRole(bankAccount);
}
return Mono.error(new Exception("No tiene permisos para registrar una cuenta bancaria"));
});
});
}
Given that you want to unit test this code, you would need to mock dependencies such as webClientCustomer.
Then you should always test whatever are the relevant paths within the code. Looking at your code I only see three relevant ones to be tested:
the method returns an empty Mono if webClientCustomer.findCustomerById(bankAccount.getCustomerId()); returns an empty Mono;
saveBankAccountAndRole(bankAccount) is called and your save() method actually returns whatever saveBankAccountAndRole(bankAccount) returns. This would should happen if webClientCustomer.isCustomerAuthorized(customerType, accountType, sizeAccounts) is true;
the method returns an exception if webClientCustomer.isCustomerAuthorized(customerType, accountType, sizeAccounts) is false.

How to make dependent webclient calls

I need to make 3 dependent WebClient API calls. In the end, I want a Mono of FinalResponse Object.
I need to use the value from the first API response to make a call to the second API (which would return Mono of Purchase class. Purchase class would contain 2 member variables
user object
List
Now for each value in the list, I need to make a third API call. and then return the final mono object to the controller.
I'm currently stuck with how to go about with an asynchronous call to 3rd API for each value in the list(returned by 2nd API)
service.getPurchases returns Mono<Purchase>. service.getSimilarItems returns Mono<List<Item>>.
class Purchase{
private List<Item> purchasedItemsList;
}
class Item {
private int itemId;
private int categoryId;
private String itemName;
}
public Mono<FinalResponse> getEndResults(UserRequest userRequest) {
Mono<User> response1 = service.getUserResponse(userRequest);
return response1.flatMap(response -> {
int userId = response.getUserId();
FinalResponse finalResponse = new FinalResponse();
List<AllItems> itemList = new LinkedList<>();
return service.getPurchase(userRequest, userId)
.map(purchasedItem -> {
val.getPurchasedItemsList().forEach(oneItem -> { // please help me how to go about from here
service.getSimilarItemsInCategory(userRequest, userId, oneItem.getCategoryId)
.map(similarItem -> {
AllItems allItem = new AllItems();
allItem.setPurchasedItem(oneItem);
allItem.setSimilarItem(similarItem);
itemList.add(allItem);
});
});
finalResponse.setResults(itemList);
return finalResponse;
});
});
}
class FinalResponse {
private User user;
private List<AllItems> results;
}
class AllItems {
private Item purchasedItem;
private List<Item> similarItem;
}
Basically the end response I need would look like
{
"users":{//UserObject//},
"results": [
{
"purchasedItem": {// Purschased Item 1},
"similarItems": [
{//Similar Item 1},
{//Similar Item 2}
]
},
{
"purchasedItem": {// Purschased Item 1},
"similarItems": [
{//Similar Item 1},
{//Similar Item 2}
]
}
]
}
Following Toerktumlare's comment: This can be fairly simple, if the WebClient calls return Monos or Fluxes of simple values or lists.
You can use flatMapMany() or flatMapIterable().
What about this simplified example?
public Mono<FinalResponse> getEndResults(UserRequest userRequest) {
Mono<User> userResponse = service.getUserResponse(userRequest);
return userResponse.flatMap(response -> {
int userId = response.getUserId();
return service.getPurchase(userRequest, userId)
.map(Purchase::getPurchasedItemsList)
.flatMapIterable(purchasedItems -> purchasedItems)
.flatMap(oneItem -> getSimilarItemInCategory(userRequest, userId, oneItem))
.collectList();
})
.map(itemList -> {
FinalResponse finalResponse = new FinalResponse();
finalResponse.setResults(itemList);
return finalResponse;
});
}
public Mono<AllItems> getSimilarItemInCategory(UserRequest userRequest, int userId, Item oneItem) {
return service.getSimilarItemsInCategory(userRequest, userId, oneItem.getCategoryId())
.map(similarItem -> {
AllItems allItem = new AllItems();
allItem.setPurchasedItem(oneItem);
allItem.setSimilarItem(similarItem);
return allItem;
});
}

Mocking a class with internal SDK calls using NSubstitute

First time trying to use NSubstitute.
I have the following method in my Web API.
For those who don't know Couchbase, lets say that a collection/bucket is like a DB table and a key is like a DB row.
Couchbase_internal.Collection_GET returns Task<ICouchbaseCollection>
I would like to write 2 unit tests.
One that tests the returned class when the key exist and one when it doesn't (couchbaseServiceResultClass).
I don't really understand where is the part where I control whether or not the key exist in the mocked data.
public class CouchbaseAPI : ControllerBase, ICouchbaseAPI
{
// GET /document_GET?bucketName=<bucketName>&key=<key>
[HttpGet]
[Consumes("application/x-www-form-urlencoded")]
[Produces(MediaTypeNames.Application.Json)]
public async Task<couchbaseServiceResultClass> document_GET([FromQuery, BindRequired] string bucketName, [FromQuery, BindRequired] string key)
{
var collection = await Couchbase_internal.Collection_GET(bucketName);
if (collection != null)
{
IGetResult result;
try
{
// get document
result = await collection.GetAsync(key);
}
catch (CouchbaseException ex)
{
return new ErrorHandling().handleCouchbaseException(ex);
}
couchbaseServiceResultClass decryptResult = new();
try
{
// decrypt document
decryptResult = Encryption.decryptContent(result);
}
catch (Exception ex)
{
return new ErrorHandling().handleException(ex, null);
}
// remove document if decryption failed
if (!decryptResult.DecryptSuccess)
{
try
{
await collection.RemoveAsync(key);
}
catch (CouchbaseException ex)
{
return new ErrorHandling().handleCouchbaseException(ex);
}
}
decryptResult.Message = "key retrieved successfully";
// return result
return decryptResult;
}
else
{
return new ErrorHandling().handleError("Collection / bucket was not found.");
}
}
This is what I have so far for the first test:
public class CouchbaseAPITests
{
private readonly CouchbaseAPI.Controllers.ICouchbaseAPI myClass = Substitute.For<CouchbaseAPI.Controllers.ICouchbaseAPI>();
[Fact]
public async Task document_GET_aKeyIsRetrievedSuccessfully()
{
// Arrange
string bucketName = "myBucket";
string keyName = "myKey";
couchbaseServiceResultClass resultClass = new();
resultClass.Success = true;
resultClass.Message = "key retrieved successfully";
myClass.document_GET(bucketName, keyName).Returns(resultClass);
// Act
var document = await myClass.document_GET(bucketName, keyName);
// Assert
Assert.True(document.Success);
Assert.Equal("key retrieved successfully", document.Message);
}
}
If we want to test that we are retrieving documents from the Couchbase API properly, then generally we want to use a real instance (local test setup) of that API where possible. If we are mocking this then our tests are not really telling us about whether our code is working correctly (just that our mock is working the way we want it to).
When certain APIs are difficult to use real instances for (e.g. non-deterministic code, difficult to reproduce conditions such as network errors, slow dependencies, etc), that's when it can be useful to introduce an interface for that dependency and to mock that for our test.
Here's a very rough example that doesn't quite match the code snippets posted, but hopefully will give you some ideas on how to proceed.
public interface IDataAdapter {
IEnumerable<IGetResult> Get(string key);
}
public class CouchbaseAdapter : IDataAdapter {
/* Implement interface for Couchbase */
}
public class AppApi {
private IDataAdapter data;
public AppApi(IDataAdapter data) {
this.data = data;
}
public SomeResult Lookup(string key) {
try {
var result = data.Get(key);
return Transform(Decrypt(result));
} catch (Exception ex) { /* error handling */ }
}
}
[Fact]
public void TestWhenKeyExists() {
var testAdapter = Substitute.For<IDataAdapter>();
var api = new AppApi(testAdapter);
testAdapter.Get("abc").Returns(/* some valid data */);
var result = api.Lookup("abc");
/* assert that result is decrypted/transformed as expected */
Assert.Equal(expectedResult, result);
}
[Fact]
public void TestWhenKeyDoesNotExist() {
var testAdapter = Substitute.For<IDataAdapter>();
var api = new AppApi(testAdapter);
var emptyData = new List<IGetResult>();
testAdapter.Get("abc").Returns(emptyData);
var result = api.Lookup("abc");
/* assert that result has handled error as expected */
Assert.Equal(expectedError, result);
}
Here we've introduced a IDataAdapter type that our class uses to abstract the details of which implementation we are using to get data. Our real code can use the CouchbaseAdapter implementation, but our tests can use a mocked version instead. For our tests, we can simulate what happens when the data adapter throws errors or returns specific information.
Note that we're only testing AppApi here -- we are not testing the CouchbaseAdapter implementation, only that AppApi will respond in a certain way if its IDataAdapter has certain behaviour. To test our CouchbaseAdapter we will want to use a real instance, but we don't have to worry about those details for testing our AppApi transformation and decryption code.

Skip chain of CompletableFuture based on a specific condition

There are several CompletionFutures methods that I'd like to chain. The problem is that I want to skip other chain and go to last method of the pipeline if some condition is met. Mentioned as comment in the below code. For example:
public CompletableFuture<Void> process() {
CompletableFuture<Set<String>> service1 = new CompletableFuture();
return service1
.thenCompose(data -> {
if (!data.isEmpty()) {
SomeObject obj = prepareSomeObject();
obj.addAll(data.stream().collect(Collectors.toSet()));
//Here want to check some condition and if it meets, then go to last method of the pipeline
//Otherwise continue with the current flow
CompletableFuture<String> someFuture = new CompletableFuture<>();
CompletableFuture<String> someOtherFuture= new CompletableFuture<>();
return CompletableFuture.allOf(someFuture, someOtherFuture)
.thenApply(aVoid -> {
String user = someFuture.join();
String unit = someOtherFuture.join();
obj.setUnit(unit);
obj.setUser(user)
return obj;
})
.thenCompose(this::someMethod)
.thenCompose(this::someOtherMethod); // last method in the pipeline
}
return completedFuture(null);
});
}
I have tried something like this, Please critique on the right way to do it.
public CompletableFuture<Void> process() {
CompletableFuture<Set<String>> service1 = new CompletableFuture();
return service1
.thenCompose(data -> {
if (!data.isEmpty()) {
SomeObject obj = prepareSomeObject();
obj.addAll(data.stream().collect(Collectors.toSet()));
if(condition not match) { //condition match
CompletableFuture<String> someFuture = new CompletableFuture<>();
CompletableFuture<String> someOtherFuture= new CompletableFuture<>();
return CompletableFuture.allOf(someFuture, someOtherFuture)
.thenApply(aVoid -> {
String user = someFuture.join();
String unit = someOtherFuture.join();
obj.setUnit(unit);
obj.setUser(user)
})
.thenCompose(this::someMethod)
.thenCompose(this::someOtherMethod); // last method in the pipeline
} else return someOtherMethod(obj);
}
return completedFuture(null);
});
}

RxJava Caching Results of Network Call IDs from a Stream (Redis or similar caching solution)

I need some help with RxJava. I have an expensive network call which returns an Observable (stream of Adverts from elasticsearch). I want to cache the ID property of each emitted item (Advert) for 10 minutes (in Redis), so that subsequent calls in the following 10 minutes use the IDs from Cache to fetch the adverts from Elasticsearch.
I've got some code - which goes some way towards achieving the desired outcome, (credit to following blog ..
http://blog.danlew.net/2015/06/22/loading-data-from-multiple-sources-with-rxjava/)
It can cache each emitted item from the stream, what I need is it to cache all the IDs from those items in the stream as 1 cache entry
Code so far is here https://github.com/tonymurphy/rxjava for anyone interested, snippets below
#Component
public class CachingObservable {
private final Logger logger = LoggerFactory.getLogger(CachingObservable.class);
#Autowired
private AdvertService advertService;
// Each "network" response is different
private Long requestNumber = 0L;
public Observable<Advert> getAdverts(final String location) {
Observable<Advert> memory = memory(location);
Observable<Advert> network = network(location);
Observable<Advert> networkWithSave = network.doOnNext(new Action1<Advert>() {
#Override
public void call(Advert advert) {
List<Long> ids = new ArrayList<Long>();
ids.add(advert.getId());
advertService.cache(location, ids);
}
});
// Retrieve the first source with data - concat checks in order
Observable<Advert> source = Observable.concat(memory,
networkWithSave)
.first();
return source;
}
From my understanding, the concat method is not really useful for my use case. I need to know if/when the network observable completes, I need to get the list of advert id's returned and I need to store them in the cache. I could subscribe to the network observable - but I want this to be lazy - only called if no data is found in the cache. So the following updated code doesn't work.. any ideas appreciated
public Observable<Advert> getAdverts(final String location) {
Observable<Advert> memory = memory(location);
Observable<Advert> network = network(location);
Observable<Advert> networkWithSave = network.doOnNext(new Action1<Advert>() {
#Override
public void call(Advert advert) {
List<Long> ids = new ArrayList<Long>();
ids.add(advert.getId());
advertService.cache(location, ids);
}
});
// Retrieve the first source with data - concat checks in order
Observable<Advert> source = Observable.concat(memory,
networkWithSave)
.first();
Observable<List<Advert>> listObservable = networkWithSave.toList();
final Func1<List<Advert>, List<Long>> transformer = new Func1<List<Advert>, List<Long>>() {
#Override
public List<Long> call(List<Advert> adverts) {
List<Long> ids = new ArrayList<Long>();
for (Advert advert : adverts) {
ids.add(advert.getId());
}
return ids;
}
};
listObservable.map(transformer).subscribe(new Action1<List<Long>>() {
#Override
public void call(List<Long> ids) {
logger.info("ids {}", ids);
}
});
return source;
}
What I would do is use filter to make sure the old content of the cache is not emitted so the concat jumps to the network call:
Subject<Pair<Long, List<Advert>>, Pair<Long, List<Advert>>> cache =
BehaviorSubject.create().toSerialized();
static final long RETENTION_TIME = 10L * 60 * 1000;
Observable<Advert> memory = cache.filter(v ->
v.first + RETENTION_TIME > System.currentTimeMillis()).flatMapIterable(v -> v);
Observable<Advert> network = ...
Observable<Advert> networkWithSave = network.toList().doOnNext(v ->
cache.onNext(Pair.of(System.currentTimeMillis(), v)).flatMapIterable(v -> v)
);
return memory.switchIfEmpty(network);
Ok, I think I have a solution which will work for me. I may have overlooked something, but it should be easy?
public Observable<Advert> getAdverts(final String location) {
Observable<Advert> memory = memory(location);
final Observable<Advert> network = network(location);
final Func1<List<Advert>, List<Long>> advertToIdTransformer = convertAdvertsToIds();
memory.isEmpty().subscribe(new Action1<Boolean>() {
#Override
public void call(Boolean aBoolean) {
if (aBoolean.equals(Boolean.TRUE)) {
Observable<List<Long>> listObservable = network.toList().map(advertToIdTransformer);
listObservable.subscribe(new Action1<List<Long>>() {
#Override
public void call(List<Long> ids) {
logger.info("Caching ids {}", ids);
advertService.cache(location, ids);
}
});
}
}
});
// Retrieve the first source with data - concat checks in order
Observable<Advert> source = Observable.concat(memory,
network)
.first();
return source;
}

Resources