Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding application class for health checking #152

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions core/src/main/java/org/microshed/testing/MicroShedApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package org.microshed.testing;

import org.microshed.testing.health.Health;
import org.microshed.testing.jaxrs.JsonBProvider;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import java.net.URI;
import java.util.Objects;

/**
* Provides access to an application that implements MicroProfile Health.
* This class can be sub-classed and extended for type-safe, business-specific methods within the project's test scope.
* <p>
* <p>
* Usage:
* <pre>
* MicroShedApplication app = MicroShedApplication.withBaseUri(baseUri).build();
* Health health = app.health();
* </pre>
* <p>
* Or a sub-classed version:
* <pre>
* class MyApplication extends MicroShedApplication {
*
* MyApplication() {
* super(URI.create("http://my-app:8080/"));
* }
*
* // add business-related methods
*
* public List<MyCustomer> getMyCustomers { ... }
* }
*
* // in the test code, access health checks, metrics, or business-related methods
*
* MyApplication app = new MyApplication();
* Health health = app.health();
* ...
* app.getMyCustomers();
* ...
* </pre>
*/
public class MicroShedApplication {

private static final String HEALTH_PATH = "/health";

private final String healthPath;

protected final Client client;
protected final WebTarget rootTarget;

protected MicroShedApplication(URI baseUri) {
this(baseUri, HEALTH_PATH);
}

private MicroShedApplication(URI baseUri, String healthPath) {
this.healthPath = healthPath;

client = ClientBuilder.newBuilder()
.register(JsonBProvider.class)
.build();
this.rootTarget = client.target(baseUri);
}

public Health health() {
return rootTarget.path(healthPath)
.request(MediaType.APPLICATION_JSON_TYPE)
.get(Health.class);
}

public static Builder withBaseUri(String baseUri) {
Objects.requireNonNull(baseUri, "Base URI must not be null");
Builder builder = new Builder();
builder.baseUri = URI.create(baseUri);
return builder;
}

public static Builder withBaseUri(URI baseUri) {
Objects.requireNonNull(baseUri, "Base URI must not be null");
Builder builder = new Builder();
builder.baseUri = baseUri;
return builder;
}

public static class Builder {

private URI baseUri;
private String healthPath = HEALTH_PATH;

public Builder healthPath(String healthPath) {
this.healthPath = healthPath;
return this;
}

public MicroShedApplication build() {
return new MicroShedApplication(baseUri, healthPath);
}
}
}
25 changes: 25 additions & 0 deletions core/src/main/java/org/microshed/testing/health/Health.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.microshed.testing.health;

import java.util.ArrayList;
import java.util.List;

public class Health {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this class is the only new class needed and we could remove the MicroShedApplication class entirely. A lot of what the MicroShedApplication does is already done via the ApplicationContainer class (such as knowing hostname, port, and app context root).

If we annotated this class with JAX-RS annotations, then users could inject and use it just like other REST clients from their application. For example:

@MicroShedTest
public class MyServiceTest {

  @Container
  public static ApplicationContainer app = // ...

  @RESTClient
  public static Health healthCheck; // the org.microshed.testing.health.Health instance

  @RESTClient
  public static MyService myService; // some REST endpoint inside the app under test

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what'd be valuable from my PoV is that folks can programmatically create a class that provides access to the app/system, especially if they can extend it and use it as a nice delegate / abstraction layer to hide complexity. The JAX-RS interfaces kind of go into that direction, but not always fully. Also, I'd love some way to enable health without @MicroShedTest (think of it similar to what RestClientBuilder does).

There's certainly room for adding a solely declarative way, like you're proposing, I also thought about that. However, when is the health check performed, if it's injected here, especially for manual envs? At test startup time? I think that'd make more sense if the user has some interface / "proxy object" to query: "please perform the health check now".


public Status status;
public List<Check> checks = new ArrayList<>();

public Check getCheck(String name) {
return checks.stream()
.filter(c -> c.name.equalsIgnoreCase(name))
.findAny().orElse(null);
}

public static class Check {
public String name;
public Status status;
}

public enum Status {
UP, DOWN;
}
}
18 changes: 18 additions & 0 deletions sample-apps/liberty-app/src/main/java/org/example/app/Health.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.example.app;

import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Readiness;

import javax.enterprise.context.ApplicationScoped;

@Readiness
@ApplicationScoped
public class Health implements HealthCheck {

@Override
public HealthCheckResponse call() {
return HealthCheckResponse.named("test-app").up().build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.example.app;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.microshed.testing.health.Health;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

/**
* Requires an local environment which is already running.
* Thus enables to decouple the life cycles of the tests and results in a smaller turnaround time.
* <p>
* Note that this is a different approach to {@link LibertySmokeIT}.
*/
public class LibertyAppSmokeIT {

private TestApplication application;

@BeforeEach
void setUp() {
application = new TestApplication();
}

@Test
void testIsSystemUp() {
assertThat(application.health().status, is(org.microshed.testing.health.Health.Status.UP));
assertThat(application.health().getCheck("test-app").status, is(Health.Status.UP));
}

@Test
void testGetAllPeople() {
assertThat(application.getAllPeople().size(), is(2));
}

@Test
void testGetPerson() {
Person person = application.getAllPeople().iterator().next();

assertThat(application.getPerson(person.id), is(person));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.example.app;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.microshed.testing.MicroShedApplication;
import org.microshed.testing.health.Health;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

/**
* Requires an local environment which is already running at {@code localhost:9080}.
* Thus enables to decouple the life cycles of the tests and results in a smaller turnaround time.
* <p>
* Note that this is a different approach to {@link LibertyAppSmokeIT}.
*/
public class LibertySmokeIT {

private MicroShedApplication application;

@BeforeEach
void setUp() {
application = MicroShedApplication.withBaseUri("http://localhost:9080").build();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the application is already running at localhost:9080 that's fine, but we shouldn't hard-code that into the test source. Instead, the host and port should be set with microshed_hostname and microshed_http_port system/env properties and then many sources (such as ApplicationContainer and RestClientBuilder) can auto-detect those values for "local dev" mode, but also switch to running with Testcontainers instances for when this runs in TravisCI.

That last part (also running in CI) is important IMO. If I understand this code correctly, these tests would fail in CI because they require something else to start the server?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, wasn't sure how to fix that, please change so it doesn't interfere with the projects pipeline. In a real-world project I'd also rather make it aware of e.g. System properties or so :)

}

@Test
void testIsSystemUp() {
assertThat(application.health().status, is(Health.Status.UP));
assertThat(application.health().getCheck("test-app").status, is(Health.Status.UP));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.example.app;

import org.microshed.testing.MicroShedApplication;
import org.microshed.testing.jaxrs.RestClientBuilder;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.Collection;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

public class TestApplication extends MicroShedApplication {

private PersonService personClient;

public TestApplication() {
super(URI.create("http://localhost:9080"));

// we might use either the type-safe RestClient or the rootTarget and client contained in MicroShedApplication

personClient = new RestClientBuilder()
.withAppContextRoot("http://localhost:9080/myservice")
.build(PersonService.class);
}

public Collection<Person> getAllPeople() {
// different approach to built-in client
return personClient.getAllPeople();
}

public Person getPerson(long id) {
// different approach to person client
Response response = rootTarget.path("/myservice")
.path(String.valueOf(id))
.request(MediaType.APPLICATION_JSON_TYPE)
.get();

assertThat(response.getStatus(), is(200));

return response.readEntity(Person.class);
}

}