diff --git a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistryAssert.java b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistryAssert.java index 89f65dc984..014e043dd9 100644 --- a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistryAssert.java +++ b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistryAssert.java @@ -15,11 +15,13 @@ */ package io.micrometer.observation.tck; +import io.micrometer.common.docs.KeyName; import io.micrometer.observation.Observation; import org.assertj.core.api.ThrowingConsumer; import java.util.List; import java.util.Objects; +import java.util.function.Consumer; import java.util.stream.Collectors; /** @@ -104,6 +106,10 @@ private String observationNames(List m.getContext().getName()).collect(Collectors.joining(",")); } + private String observations() { + return this.actual.getContexts().stream().map(m -> m.getContext().toString()).collect(Collectors.joining(",")); + } + /** * Verifies that there's at least one {@link Observation} with a given name (ignoring * case) and continues assertions for the first found one. @@ -151,6 +157,240 @@ public TestObservationRegistryAssert hasHandledContextsThatSatisfy( return this; } + /** + * Provides verification for all Observations having the given name. + *

+ * Examples:

 // assertions succeed
+     * assertThat(testObservationRegistry).forAllObservationsWithNameEqualTo("foo", ObservationContextAssert -> ObservationContextAssert.hasError());
+     *
+     * // assertions fail - assuming that there was a foo observation but none had errors
+     * assertThat(testObservationRegistry).forAllObservationsWithNameEqualTo("foo", ObservationContextAssert -> ObservationContextAssert.hasError());
+ * @param name searched Observation name + * @param observationConsumer assertion to be executed for each Observation + * @return {@code this} assertion object. + * @throws AssertionError if the actual value is {@code null}. + * @throws AssertionError if there is no Observation with the given name + * @throws AssertionError if there is an Observation with the given name but the + * additional assertion is not successful + * @since 1.0.0 + */ + @SuppressWarnings("rawtypes") + public TestObservationRegistryAssert forAllObservationsWithNameEqualTo(String name, + Consumer observationConsumer) { + isNotNull(); + hasObservationWithNameEqualTo(name); + this.actual.getContexts().stream().filter(f -> name.equals(f.getContext().getName())) + .forEach(f -> observationConsumer.accept(ObservationContextAssert.then(f.getContext()))); + return this; + } + + /** + * Provides verification for all Observations having the given name (ignoring case). + *

+ * Examples:

 // assertions succeed
+     * assertThat(testObservationRegistry).forAllObservationsWithNameEqualTo("foo", ObservationContextAssert -> ObservationContextAssert.hasError());
+     *
+     * // assertions fail - assuming that there was a foo observation but none had errors
+     * assertThat(testObservationRegistry).forAllObservationsWithNameEqualTo("foo", ObservationContextAssert -> ObservationContextAssert.hasError());
+ * @param name searched Observation name (ignoring case) + * @param observationConsumer assertion to be executed for each Observation + * @return {@code this} assertion object. + * @throws AssertionError if the actual value is {@code null}. + * @throws AssertionError if there is no Observation with the given name (ignoring + * case) + * @throws AssertionError if there is a Observation with the given name (ignoring + * case) but the additional assertion is not successful + * @since 1.0.0 + */ + @SuppressWarnings("rawtypes") + public TestObservationRegistryAssert forAllObservationsWithNameEqualToIgnoreCase(String name, + Consumer observationConsumer) { + isNotNull(); + hasObservationWithNameEqualToIgnoringCase(name); + this.actual.getContexts().stream().filter(f -> name.equalsIgnoreCase(f.getContext().getName())) + .forEach(f -> observationConsumer.accept(ObservationContextAssert.then(f.getContext()))); + return this; + } + + /** + * Verifies that there is a proper number of Observations. + *

+ * Examples:

 // assertions succeed
+     * assertThat(testObservationRegistry).hasNumberOfObservationsEqualTo(1);
+     *
+     * // assertions fail - assuming that there was only 1 observation
+     * assertThat(testObservationRegistry).hasNumberOfObservationsEqualTo(2);
+ * @param expectedNumberOfObservations expected number of Observations + * @return {@code this} assertion object. + * @throws AssertionError if the actual value is {@code null}. + * @throws AssertionError if the number of Observations is different from the desired + * one + * @since 1.0.0 + */ + public TestObservationRegistryAssert hasNumberOfObservationsEqualTo(int expectedNumberOfObservations) { + isNotNull(); + if (this.actual.getContexts().size() != expectedNumberOfObservations) { + failWithMessage("There should be <%s> Observations but there were <%s>. Found following Observations \n%s", + expectedNumberOfObservations, this.actual.getContexts().size(), + observationNames(this.actual.getContexts())); + } + return this; + } + + /** + * Verifies that there is a proper number of Observations with the given name. + *

+ * Examples:

 // assertions succeed
+     * assertThat(testObservationRegistry).hasNumberOfObservationsWithNameEqualTo("foo", 1);
+     *
+     * // assertions fail - assuming that there is only 1 observation with that name
+     * assertThat(testObservationRegistry).hasNumberOfObservationsWithNameEqualTo("foo", 2);
+ * @param observationName Observation name + * @param expectedNumberOfObservations expected number of Observations with the given + * name + * @return {@code this} assertion object. + * @throws AssertionError if the actual value is {@code null}. + * @throws AssertionError if the number of properly named Observations is different + * from the desired one + * @since 1.0.0 + */ + public TestObservationRegistryAssert hasNumberOfObservationsWithNameEqualTo(String observationName, + int expectedNumberOfObservations) { + isNotNull(); + long observationsWithNameSize = this.actual.getContexts().stream() + .filter(f -> observationName.equals(f.getContext().getName())).count(); + if (observationsWithNameSize != expectedNumberOfObservations) { + failWithMessage( + "There should be <%s> Observations with name <%s> but there were <%s>. Found following Observations \n%s", + expectedNumberOfObservations, observationName, observationsWithNameSize, + observationNames(this.actual.getContexts())); + } + return this; + } + + /** + * Verifies that there is a proper number of Observations with the given name + * (ignoring case). + *

+ * Examples:

 // assertions succeed
+     * assertThat(testObservationRegistry).hasNumberOfObservationsWithNameEqualToIgnoreCase(1);
+     *
+     * // assertions fail - assuming that there's only 1 such observation
+     * assertThat(testObservationRegistry).hasNumberOfObservationsWithNameEqualToIgnoreCase(2);
+ * @param observationName Observation name + * @param expectedNumberOfObservations expected number of Observations with the given + * name (ignoring case) + * @return {@code this} assertion object. + * @throws AssertionError if the actual value is {@code null}. + * @throws AssertionError if the number of properly named Observations is different + * from the desired one + * @since 1.0.0 + */ + public TestObservationRegistryAssert hasNumberOfObservationsWithNameEqualToIgnoreCase(String observationName, + int expectedNumberOfObservations) { + isNotNull(); + long observationsWithNameSize = this.actual.getContexts().stream() + .filter(f -> observationName.equalsIgnoreCase(f.getContext().getName())).count(); + if (observationsWithNameSize != expectedNumberOfObservations) { + failWithMessage( + "There should be <%s> Observations with name (ignoring case) <%s> but there were <%s>. Found following Observations \n%s", + expectedNumberOfObservations, observationName, observationsWithNameSize, + observationNames(this.actual.getContexts())); + } + return this; + } + + /** + * Verifies that there is a Observation with a key value. + *

+ * Examples:

 // assertions succeed
+     * assertThat(testObservationRegistry).hasAnObservationWithAKeyValue("foo", "bar");
+     *
+     * // assertions fail - assuming that there is no such tags in any observation
+     * assertThat(testObservationRegistry).hasAnObservationWithAKeyValue("foo", "bar");
+ * @param key expected tag key + * @param value expected tag value + * @return {@code this} assertion object. + * @throws AssertionError if the actual value is {@code null}. + * @throws AssertionError if there is no Observation with given tag key and value + * @since 1.0.0 + */ + public TestObservationRegistryAssert hasAnObservationWithAKeyValue(String key, String value) { + isNotNull(); + this.actual.getContexts().stream().flatMap(f -> f.getContext().getAllKeyValues().stream()) + .filter(keyValue -> keyValue.getKey().equals(key) && keyValue.getValue().equals(value)).findFirst() + .orElseThrow(() -> { + failWithMessage( + "There should be at least one Observation with tag key <%s> and value <%s> but found none. Found following Observations \n%s", + key, value, observations()); + return new AssertionError(); + }); + return this; + } + + /** + * Verifies that there is a Observation with a key value key. + *

+ * Examples:

 // assertions succeed
+     * assertThat(testObservationRegistry).hasAnObservationWithAKeyName("foo");
+     *
+     * // assertions fail - assuming that the observation doesn't have a key value with such a key
+     * assertThat(testObservationRegistry).hasAnObservationWithAKeyName("foo");
+ * @param key expected tag key + * @return {@code this} assertion object. + * @throws AssertionError if the actual value is {@code null}. + * @throws AssertionError if there is no Observation with given tag key + * @since 1.0.0 + */ + public TestObservationRegistryAssert hasAnObservationWithAKeyName(String key) { + isNotNull(); + this.actual.getContexts().stream().flatMap(f -> f.getContext().getAllKeyValues().stream()) + .filter(keyValue -> keyValue.getKey().equals(key)).findFirst().orElseThrow(() -> { + failWithMessage( + "There should be at least one Observation with tag key <%s> but found none. Found following Observations \n%s", + key, observations()); + return new AssertionError(); + }); + return this; + } + + /** + * Verifies that there is a Observation with a tag. + *

+ * Examples:

 // assertions succeed
+     * assertThat(testObservationRegistry).hasAnObservationWithAKeyValue(SomeKeyName.FOO, "bar");
+     *
+     * // assertions fail - assuming that the observation doesn't have such a key value
+     * assertThat(testObservationRegistry).hasAnObservationWithAKeyValue(SomeKeyName.FOO, "baz");
+ * @param key expected tag key + * @param value expected tag value + * @return {@code this} assertion object. + * @throws AssertionError if the actual value is {@code null}. + * @throws AssertionError if there is no Observation with given tag key + * @since 1.0.0 + */ + public TestObservationRegistryAssert hasAnObservationWithAKeyValue(KeyName key, String value) { + return hasAnObservationWithAKeyValue(key.asString(), value); + } + + /** + * Verifies that there is a Observation with a tag key. + *

+ * Examples:

 // assertions succeed
+     * assertThat(testObservationRegistry).hasAnObservationWithAKeyName(SomeKeyName.FOO);
+     *
+     * // assertions fail - assuming that the observation doesn't have such a key name
+     * assertThat(testObservationRegistry).hasAnObservationWithAKeyName(SomeKeyName.FOO);
+ * @param key expected tag key + * @return {@code this} assertion object. + * @throws AssertionError if the actual value is {@code null}. + * @throws AssertionError if there is no Observation with given tag key + * @since 1.0.0 + */ + public TestObservationRegistryAssert hasAnObservationWithAKeyName(KeyName key) { + return hasAnObservationWithAKeyName(key.asString()); + } + /** * Provides assertions for {@link Observation} and allows coming back to * {@link TestObservationRegistryAssert}. @@ -168,7 +408,7 @@ private That(TestObservationRegistry.TestObservationContext testContext, } /** - * Synactic sugar to smoothly go to + * Syntactic sugar to smoothly go to * {@link TestObservationRegistryAssertReturningObservationContextAssert}. * @return {@link TestObservationRegistryAssertReturningObservationContextAssert} * assert object diff --git a/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/TestObservationRegistryAssertTests.java b/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/TestObservationRegistryAssertTests.java index e78f799f55..f2f90beacd 100644 --- a/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/TestObservationRegistryAssertTests.java +++ b/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/TestObservationRegistryAssertTests.java @@ -15,6 +15,7 @@ */ package io.micrometer.observation.tck; +import io.micrometer.common.docs.KeyName; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import org.assertj.core.api.Assertions; @@ -129,6 +130,200 @@ void should_not_fail_when_contexts_satisfy_the_assertions() { .hasHandledContextsThatSatisfy(contexts -> Assertions.assertThat(contexts).hasSize(1))); } + @Test + void should_fail_when_there_are_observations() { + Observation.createNotStarted("foo", registry).start().stop(); + + thenThrownBy(() -> TestObservationRegistryAssert.assertThat(registry).doesNotHaveAnyObservation()) + .isInstanceOf(AssertionError.class); + } + + @Test + void should_not_fail_when_there_are_no_observations() { + thenNoException() + .isThrownBy(() -> TestObservationRegistryAssert.assertThat(registry).doesNotHaveAnyObservation()); + } + + @Test + void should_fail_when_there_is_no_observation_with_name() { + Observation.createNotStarted("foo", registry).start().stop(); + + thenThrownBy(() -> TestObservationRegistryAssert.assertThat(registry).forAllObservationsWithNameEqualTo("bar", + ObservationContextAssert::doesNotHaveError)).isInstanceOf(AssertionError.class); + } + + @Test + void should_fail_when_all_observations_match_the_assertion() { + Observation.createNotStarted("foo", registry).start().stop(); + + thenThrownBy(() -> TestObservationRegistryAssert.assertThat(registry).forAllObservationsWithNameEqualTo("foo", + ObservationContextAssert::hasError)).isInstanceOf(AssertionError.class); + } + + @Test + void should_not_fail_when_all_observations_match_the_assertion() { + Observation.createNotStarted("foo", registry).start().stop(); + + thenNoException().isThrownBy(() -> TestObservationRegistryAssert.assertThat(registry) + .forAllObservationsWithNameEqualTo("foo", ObservationContextAssert::doesNotHaveError)); + } + + @Test + void should_fail_when_there_is_no_observation_with_name_ignore_case() { + Observation.createNotStarted("FOO", registry).start().stop(); + + thenThrownBy(() -> TestObservationRegistryAssert.assertThat(registry) + .forAllObservationsWithNameEqualToIgnoreCase("bar", ObservationContextAssert::doesNotHaveError)) + .isInstanceOf(AssertionError.class); + } + + @Test + void should_fail_when_not_all_observations_match_the_assertion_ignore_case() { + Observation.createNotStarted("FOO", registry).start().stop(); + + thenThrownBy(() -> TestObservationRegistryAssert.assertThat(registry) + .forAllObservationsWithNameEqualToIgnoreCase("foo", ObservationContextAssert::hasError)) + .isInstanceOf(AssertionError.class); + } + + @Test + void should_not_fail_when_all_observations_match_the_assertion_ignore_case() { + Observation.createNotStarted("FOO", registry).start().stop(); + + thenNoException().isThrownBy(() -> TestObservationRegistryAssert.assertThat(registry) + .forAllObservationsWithNameEqualToIgnoreCase("foo", ObservationContextAssert::doesNotHaveError)); + } + + @Test + void should_fail_when_number_of_observations_does_not_match() { + Observation.createNotStarted("FOO", registry).start().stop(); + + thenThrownBy(() -> TestObservationRegistryAssert.assertThat(registry).hasNumberOfObservationsEqualTo(0)) + .isInstanceOf(AssertionError.class); + } + + @Test + void should_not_fail_when_number_of_observations_matches() { + Observation.createNotStarted("FOO", registry).start().stop(); + + thenNoException() + .isThrownBy(() -> TestObservationRegistryAssert.assertThat(registry).hasNumberOfObservationsEqualTo(1)); + } + + @Test + void should_fail_when_number_is_correct_but_names_dont_match() { + Observation.createNotStarted("foo", registry).start().stop(); + + thenThrownBy(() -> TestObservationRegistryAssert.assertThat(registry) + .hasNumberOfObservationsWithNameEqualTo("foo", 0)).isInstanceOf(AssertionError.class); + } + + @Test + void should_fail_when_number_is_incorrect_but_names_match() { + Observation.createNotStarted("foo", registry).start().stop(); + + thenThrownBy(() -> TestObservationRegistryAssert.assertThat(registry) + .hasNumberOfObservationsWithNameEqualTo("bar", 1)).isInstanceOf(AssertionError.class); + } + + @Test + void should_not_fail_when_number_and_names_match() { + Observation.createNotStarted("foo", registry).start().stop(); + + thenNoException().isThrownBy(() -> TestObservationRegistryAssert.assertThat(registry) + .hasNumberOfObservationsWithNameEqualTo("foo", 1)); + } + + @Test + void should_fail_when_number_is_correct_but_names_dont_match_ignore_case() { + Observation.createNotStarted("FOO", registry).start().stop(); + + thenThrownBy(() -> TestObservationRegistryAssert.assertThat(registry) + .hasNumberOfObservationsWithNameEqualToIgnoreCase("foo", 0)).isInstanceOf(AssertionError.class); + } + + @Test + void should_fail_when_number_is_incorrect_but_names_match_ignore_case() { + Observation.createNotStarted("FOO", registry).start().stop(); + + thenThrownBy(() -> TestObservationRegistryAssert.assertThat(registry) + .hasNumberOfObservationsWithNameEqualToIgnoreCase("bar", 1)).isInstanceOf(AssertionError.class); + } + + @Test + void should_not_fail_when_number_and_names_match_ignore_case() { + Observation.createNotStarted("FOO", registry).start().stop(); + + thenNoException().isThrownBy(() -> TestObservationRegistryAssert.assertThat(registry) + .hasNumberOfObservationsWithNameEqualToIgnoreCase("foo", 1)); + } + + @Test + void should_fail_when_key_value_not_matched() { + Observation.createNotStarted("FOO", registry).lowCardinalityKeyValue("foo", "bar").start().stop(); + + thenThrownBy( + () -> TestObservationRegistryAssert.assertThat(registry).hasAnObservationWithAKeyValue("key", "value")) + .isInstanceOf(AssertionError.class); + } + + @Test + void should_not_fail_when_key_value_matched() { + Observation.createNotStarted("FOO", registry).lowCardinalityKeyValue("foo", "bar").start().stop(); + + thenNoException().isThrownBy( + () -> TestObservationRegistryAssert.assertThat(registry).hasAnObservationWithAKeyValue("foo", "bar")); + } + + @Test + void should_fail_when_key_not_matched() { + Observation.createNotStarted("FOO", registry).lowCardinalityKeyValue("foo", "bar").start().stop(); + + thenThrownBy(() -> TestObservationRegistryAssert.assertThat(registry).hasAnObservationWithAKeyName("key")) + .isInstanceOf(AssertionError.class); + } + + @Test + void should_not_fail_when_key_matched() { + Observation.createNotStarted("FOO", registry).lowCardinalityKeyValue("foo", "bar").start().stop(); + + thenNoException().isThrownBy( + () -> TestObservationRegistryAssert.assertThat(registry).hasAnObservationWithAKeyName("foo")); + } + + @Test + void should_fail_when_key_value_not_matched_using_KeyName() { + Observation.createNotStarted("FOO", registry).lowCardinalityKeyValue("foo", "bar").start().stop(); + + thenThrownBy(() -> TestObservationRegistryAssert.assertThat(registry) + .hasAnObservationWithAKeyValue(MyKeyName.FOO, "value")).isInstanceOf(AssertionError.class); + } + + @Test + void should_not_fail_when_key_value_matched_using_KeyName() { + Observation.createNotStarted("FOO", registry).lowCardinalityKeyValue("foo", "bar").start().stop(); + + thenNoException().isThrownBy(() -> TestObservationRegistryAssert.assertThat(registry) + .hasAnObservationWithAKeyValue(MyKeyName.FOO, "bar")); + } + + @Test + void should_fail_when_key_not_matched_using_KeyName() { + Observation.createNotStarted("FOO", registry).lowCardinalityKeyValue("aaa", "bar").start().stop(); + + thenThrownBy( + () -> TestObservationRegistryAssert.assertThat(registry).hasAnObservationWithAKeyName(MyKeyName.FOO)) + .isInstanceOf(AssertionError.class); + } + + @Test + void should_not_fail_when_key_matched_using_KeyName() { + Observation.createNotStarted("FOO", registry).lowCardinalityKeyValue("foo", "bar").start().stop(); + + thenNoException().isThrownBy( + () -> TestObservationRegistryAssert.assertThat(registry).hasAnObservationWithAKeyName(MyKeyName.FOO)); + } + @Test void should_jump_to_and_back_from_context_assert() { new Example(registry).run(); @@ -154,4 +349,15 @@ void run() { } + enum MyKeyName implements KeyName { + + FOO { + @Override + public String asString() { + return "foo"; + } + } + + } + }