Sunday, August 13, 2023

Micronaut Series: How to write and test HTTP REST Clients

Creating a Micronaut project

For the purposes of demonstrating the concepts we are going to create a Micronaut Java application that exposes a simple REST service.
To save some typing headfirst to the Microsoft launcher site, Microsoft Launch and choose 

Application Type: Micronaut Application
Micronaut Version: 4.0.3
Language: Java , Version: 17
Build Tool: Maven
Test Framework: JUnit
Included Features: http-client, reactor, awaitility, annotation-api, reactor-http-client, serialization-jackson
Generate project with Launch



REST Controller

Our PersonController controller will serve REST resources as follows:
URI    Verb    Purpose
"/person/{id}"    GET    getting one Person by ID
"person/all"        GET    getting all Person items
"/person"           POST  creates a new Person
Here's the class definition:
@Controller("/person")
public class PersonController {
@Inject
PersonService personService;

@Post
public Person add(@Body Person person) {
return personService.add(person);
}

@Get("/{id}")
public Optional<Person> findById(@PathVariable Integer id) {
return personService.getPersonList().stream()
.filter(person -> person.id().equals(id))
.findFirst();
}

@Get(value = "all", produces = MediaType.APPLICATION_JSON)
public List<Person> findAll() {
return personService.getPersonList();
}
}

Declarative clients

Declarative HTTP clients are introduced by the annotation @Client on either an interface or an abstract class. For the purposes of this demo I am going to use it with an interface PersonClient:
@Client("/person")
public interface PersonClient {
@Post
Person add(@Body Person person);

@Get("{id}")
Optional<Person> findById(@PathVariable Integer id);

@Get(value = "all", consumes = MediaType.APPLICATION_JSON)
List<Person> findAll();

}

Microsoft will create the client implementation for me using some fairy AOP magic dust. The JUnit Micronaut test is pretty straightforward:
@MicronautTest // (1)
public class PersonControllerTest {

@Inject
EmbeddedServer server; // (2)
@Inject
PersonClient client; // (3)

@Test
public void testAddDeclarative() {
final Person person = new Person(null, "First Last", 22);
Person s = client.add(person);
assertThat(s.id(), is(3)); // (4)
}

@Test
void testFindByIdDeclarative() {
Optional<Person> optionalPerson = client.findById(1);
assertThat(optionalPerson.isPresent(), is(true));
}

@Test
void testFindAllStreamDeclarative() {
List<Person> list = client.findAll();
assertThat(list, hasSize(2));
}
}

Explanations:
  1. The annotation @MicronautTest will add the necessary wiring so that our test knows about the Micronaut's application context and allows us to use injection.
  2. Injects an instance of our application as an embedded server.
  3. Injects the HTTP client created at compilation time using AOP.
  4. Asserts that the new person added has the ID=3 because there are already two people in the store.

Using Reactive processing

If we want to use reactive processing we need to make some adjustments. First our controller becomes:
@Controller("/personReactive")
public class PersonReactiveController {
private static final Logger LOGGER = LoggerFactory
.getLogger(PersonReactiveController.class);

@Inject
PersonService personService;

@Post
public Mono<Person> add(@Body Person person) {
Person newPerson = personService.add(person);
return Mono.just(newPerson); // (1)
}

@Get("/{id}")
public Publisher<Optional<Person>> findById(@PathVariable Integer id) {
return Publishers.just(personService.getPersonList()
.stream()
.filter(person -> person.id().equals(id))
.findAny()
); // (2)
}

@Get(value = "stream", produces = MediaType.APPLICATION_JSON_STREAM)
public Flux<Person> findAllStream() {
return Flux.fromIterable(personService.getPersonList())
.doOnNext(person -> LOGGER.info("Server: {}", person)); // (3)
}
}
Then our reactive declarative client:
@Client("/personReactive")
public interface PersonReactiveClient {
@Post
Mono<Person> add(@Body Person person);

@Get("{id}")
Publisher<Person> findById(@PathVariable Integer id);

@Get(value = "stream", consumes = MediaType.APPLICATION_JSON_STREAM)
Flux<Person> findAllStream();
}
Explanations:
For both server and client I am using Reactive Streams Specification and project-reactor.
  1.  Returning a Mono<Person> (import reactor.core.publisher.Mono) is the most appropriate thing to do here, since Mono is a Publisher that emits at most one value.
  2. Here the return value indicates that the publisher will emit an unbounded number of  Optional<Person> elements. I could have used instead Mono<Person> or Mono<Optional<Person>> since findById is supposed to return a single value (or nothing). (import org.reactivestreams.Publisher)
  3. Flux is a variant of Publisher that emits 0 to N elements and then it completes (successfully or not). (import reactor.core.publisher.Flux) 

Testing reactive client

Testing reactive becomes a tad more complex since we are now dealing with an asynchronous flow. Luckily Awaitility comes to the rescue:
@MicronautTest
public class PersonReactiveControllerTest {
private static final Logger LOGGER = LoggerFactory
        .getLogger(PersonReactiveControllerTest.class);

@Inject
EmbeddedServer server;
@Inject
PersonReactiveClient client;


@Test
public void testAddDeclarative() {
final AtomicBoolean flip = new AtomicBoolean(false);
final Person person = new Person(null, "First Last", 22);
Mono<Person> s = client.add(person);
s.subscribe(person1 -> {
LOGGER.info("Added: {}", person1);
assertThat(person1.id(), is(3));
flip.set(true);
});
await().atMost(200, TimeUnit.MILLISECONDS).until(flip::get); // (1)

}

@Test
void testFindByIdDeclarative() {
final AtomicBoolean flip = new AtomicBoolean(false);
Publisher<Person> publisher = client.findById(1);
publisher.subscribe(new Subscriber<Person>() {
@Override
public void onSubscribe(Subscription s) {
s.request(1); //(2)
}

@Override
public void onNext(Person person) {
LOGGER.info("Received a person: {}", person);
assertThat(person.id(), is(1)); // (3)
}

@Override
public void onError(Throwable t) {
LOGGER.error("Something went wrong", t);
}

@Override
public void onComplete() {
flip.set(true); // (4)
}
});
await().atMost(300, TimeUnit.MILLISECONDS).until(flip::get);
}
//other methods omitted for brevity
}
Explanations:
  1. Here I am using await() to wait until flip gets flipped to True (this occurs when the subscriber Lambda receives the emitted Person). The waiting time is bounded by the atMost call (200 millisecs).
  2. The anonymous subscriber requests a new item from the publisher upon receiving the onSubscribe event.
  3. The subscriber receives onNext event with the Person emitted by the publisher; I am asserting that the the Person's ID is in fact "1", the same value as requested by the call "findById(1)".
  4. Upon stream completion the subscriber gets the onComplete event; here I am setting the flip to True so await() can return and the test can finish.

Conclusion

This article hopefully demonstrated how to write and test declarative HTTP clients in a Micronaut application.
The project is available on GitHub

No comments: