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

Add WebTestClient #4339

Open
ikhoon opened this issue Jul 7, 2022 · 7 comments · May be fixed by #4740
Open

Add WebTestClient #4339

ikhoon opened this issue Jul 7, 2022 · 7 comments · May be fixed by #4740

Comments

@ikhoon
Copy link
Contributor

ikhoon commented Jul 7, 2022

It should be helpful to assert a response to a test request fluently.

@RegisterExtension
static ServerExtension server = ...;

@Test
void testRestApi() {
    WebTestClient client = server.webTestClient();
    client.get("/api/v1/carts")
          .execute()
          .assertStatus().isEqualTo(HttpStatus.OK) // or `isOk()` as an alias
          .assertHeaders().contain(HttpHeaderNames.CONTENT_TYPE, MediaType.JSON)
          .assertBody().contain(...)
          .assertTrailers().isEqualTo(Map.of(...));
}
@trustin
Copy link
Member

trustin commented Jul 7, 2022

Could also integrate with AssertJ and Kotlin/Scala assertions.

@ikhoon
Copy link
Contributor Author

ikhoon commented Jul 30, 2022

In addition to assertion, it would be useful to support mocking for isolated unit tests.

Related work:
https://sttp.softwaremill.com/en/latest/testing.html

val testingBackend = SttpBackendStub.synchronous
  .whenRequestMatches(_.uri.path.startsWith(List("a", "b")))
  .thenRespond("Hello there!")
  .whenRequestMatches(_.method == Method.POST)
  .thenRespondServerError()

val response1 = basicRequest.get(uri"http://example.org/a/b/c").send(testingBackend)
// response1.body will be Right("Hello there")

@tomatophobia
Copy link
Contributor

Hi @ikhoon 😄 May I work on this issue?

@ikhoon
Copy link
Contributor Author

ikhoon commented Mar 2, 2023

For sure, go ahead.

@tomatophobia
Copy link
Contributor

tomatophobia commented Mar 2, 2023

@ikhoon I have a question about implementation direction. For example, if the result type of client.get(...).execute() is defined as TestHttpResponse, I have considered two possible ways to implement this class (or interface)

The first approach is a implementation that does not depend on AssertJ:

class TestHttpResponse {

  AssertStatus assertStatus() {
    return new AssertStatus(this.status, status())
  }
}

class AssertStatus {

  HttpStatus actual;
  TestHttpResponse back;

  …constructor

  TestHttpResponse isEqualTo(HttpStatus expected) {
    checkState(actual.equals(expected), "Some message");
    return back;
  }
}

@Test
void test() {
  assertThatCode(() -> {
    client.get(…).execute()
        .assertStatus().isEqualTo(HttpStatus.OK);
  }).doesNotThrowAnyException();
}

The first approach requires creating appropriate intermediate classes, such as AssertStatus, AssertHeaders, AssertTrailers, etc. and implementing assertion methods, such as isEqualTo, isTrue, etc. manually. However, this approach enables the creation of domain-specific assertion methods such as assertStatus().isOk() that are not dependent on AssertJ.

The second approach is an implementation that depends on AssertJ.

class TestHttpResponse {
  AssertEntity<AbstractComparableAssert<?, HttpStatus>> assertStatus(HttpStatus status) {
    return new AssertEntity<AbstractComparableAssert<?, HttpStatus>>(assertThat(status), this);
  }
}

class AssertEntity<T> {

  T entity;
  TestHttpResponse back;

  …constructor

  TestHttpResponse that(Consumer<T> consumer) {
    consumer.accept(entity);
    return back;
  }
}

@Test
void test() {
  client.get(…).execute()
      .assertStatus().that(a -> a.isEqualTo(HttpStatus.OK))
}

The second approach has a simpler implementation compared to the first approach, as it only requires one intermediate class (AssertEntity) since it uses AssertJ directly. However, this approach is limited to using only the assertion methods provided by AssertJ and cannot utilize any domain-specific assertion methods.

Would you please share your thoughts on which approach you believe is better, or if neither of them is the expected approach?

@ikhoon
Copy link
Contributor Author

ikhoon commented Mar 7, 2023

If a test client depends on AssertJ, I propose to design the code like:

public final class AssertableHttpResponse {

    private final AggregatedHttpResposne response;

    public AbstractComparableAssert<...> assertStatus() {
       return asserThat(resposne.status());
    }

}

We can use this assertion by wrapping a test client with com.linecorp.armeria.testing.assertj.AssertableWebClient.

@RegisterExtension
ServerExtension server = new ServerExtension(...) { ... }

@Test
void shouldReturn200OK() {
    // Need a better name. 🤔 
    AssertableWebClient client = AssertableWebClient.of(server.webClient());
    client.get(...)
          .execute()
          .assertStatus().isEqualTo(HttpStatus.OK)
          .assertHeaders().contains(entry(HttpHeaderNames.XXX, val))
          .assertHeaders(HttpHeaderNames.YYY).isEqualTo("...")
          .assertContent().isEqualTo("Resposne body");
}

As armeria-junit5 does not rely on AssertJ, we may need to create armeria-assertj module and implement the AssertJ-based test client there.

The first approach is a implementation that does not depend on AssertJ:

It is also a good approach because a test client is can be created from ServerExtension.
The design sounds good to me.

@ikhoon
Copy link
Contributor Author

ikhoon commented Apr 2, 2024

In addition to fluent assertions, we may provide a way to set mock responses to the WebTestClient.

WebTestClient client = WebTestClient.mock();
client.expect("/foo", HttpResponse.of(OK));
assert client.get("/foo").status() == HttpStatus.OK;
assert client.get("/bar").status() == HttpStatus.NOT_FOUND;

This API will be useful when the server is unavailable in test env or difficult to make idempotent responses.

Related work: https://github.com/fabric8io/kubernetes-client/blob/main/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestStandardHttpClient.java#L104-L108

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants