Skip to content

Commit

Permalink
merge: #8797
Browse files Browse the repository at this point in the history
8797: Extend Either/EitherAssert capabilities r=npepinpe a=npepinpe

## Description

This PR extends `Either` by adding a new API, `Either#getOrElse(R)`. This allows to extract the right value of the `Either` or returning a fallback. I did not add any tests as the implementation is incredibly simple, and I can't foresee ever getting more complex, but do challenge this.

It also extends the related `EitherAssert` by adding a new adding new `left` and `right` extraction capabilities. So you can now assert something like:

```java
EitherAssert.assertThat(either).left().isEqualTo(1);
EitherAssert.assertThat(instantEither)
	.right()
	.asInstanceOf(InstanceOfAssertFactories.INSTANT)
	.isBetween(today, tomorrow);
```

Note that calling `EitherAssert#right()` will, under the hood, still call `EitherAssert#isRight()`.

This PR is related to #5525 and is extracted from the bigger spike in #8491. You can review how it's used there, specifically in the `JobBatchCollectorTest`. As such, this is marked for backporting, since we'll backport the complete fix for #5525.

## Related issues

related to #5525 



Co-authored-by: Nicolas Pepin-Perreault <nicolas.pepin-perreault@camunda.com>
Co-authored-by: Nicolas Pepin-Perreault <43373+npepinpe@users.noreply.github.com>
  • Loading branch information
3 people committed Feb 17, 2022
2 parents 5533dc5 + 873160f commit 5ec45b8
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 163 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
import io.camunda.zeebe.protocol.impl.encoding.ExecuteQueryResponse;
import io.camunda.zeebe.protocol.record.ErrorCode;
import io.camunda.zeebe.protocol.record.ValueType;
import io.camunda.zeebe.test.util.asserts.EitherAssert;
import io.camunda.zeebe.transport.ServerOutput;
import io.camunda.zeebe.util.Either;
import io.camunda.zeebe.util.EitherAssert;
import io.camunda.zeebe.util.buffer.BufferUtil;
import io.camunda.zeebe.util.sched.ActorScheduler;
import java.util.Optional;
Expand Down
6 changes: 6 additions & 0 deletions test-util/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@
<scope>compile</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>

</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,95 @@

import io.camunda.zeebe.util.Either;
import org.assertj.core.api.AbstractObjectAssert;
import org.assertj.core.api.AssertFactory;
import org.assertj.core.api.InstanceOfAssertFactory;
import org.assertj.core.api.ObjectAssert;

/**
* AssertJ assertions to help you test {@link Either} instances. Typically, you will check if it is
* of the expected side using {@link #isLeft()} or {@link #isRight()}, and then extract that value
* using {@link #left()} and {@link #right()} to assert properties on the actual result. You can
* further chain these with a standard {@link #asInstanceOf(InstanceOfAssertFactory)} to get a more
* specific type.
*
* @param <L> the left type
* @param <R> the right type
*/
public final class EitherAssert<L, R>
extends AbstractObjectAssert<EitherAssert<L, R>, Either<L, R>> {
public EitherAssert(final Either<L, R> actual) {
super(actual, EitherAssert.class);
}

/**
* Convenience factory method which you can use as an {@link AssertFactory}.
*
* @param actual the actual object under test
* @param <L> the left type
* @param <R> the right type
* @return an instance of {@link EitherAssert} on {@code actual}
*/
public static <L, R> EitherAssert<L, R> assertThat(final Either<L, R> actual) {
return new EitherAssert<>(actual);
}

/**
* Fails if {@link #actual} has no right member or is null.
*
* @return itself for chaining
*/
public EitherAssert<L, R> isRight() {
isNotNull();

if (actual.isLeft()) {
failWithMessage("Expected <%s> to be right, but was left <%s>", actual, actual.getLeft());
}

return myself;
}

/**
* Extracts the right member of {@link #actual} and returns an {@link ObjectAssert} wrapping it.
* Fails if {@link #actual} has no right member or is null.
*
* @return a new {@link ObjectAssert} around the right member of {@link #actual}
*/
public ObjectAssert<R> right() {
isNotNull();

// use asInstanceOf to preserve the current assertion info and description
return asInstanceOf(
new InstanceOfAssertFactory<>(
Either.Right.class, object -> new ObjectAssert<>(actual.get())));
}

/**
* Fails if {@link #actual} has no left member or is null.
*
* @return itself for chaining
*/
public EitherAssert<L, R> isLeft() {
isNotNull();

if (actual.isRight()) {
failWithMessage("Expected <%s> to be left, but was right <%s>", actual, actual.get());
}

return myself;
}

/**
* Extracts the left member of {@link #actual} and returns an {@link ObjectAssert} wrapping it.
* Fails if {@link #actual} has no left member or is null.
*
* @return a new {@link ObjectAssert} around the left member of {@link #actual}
*/
public ObjectAssert<L> left() {
isNotNull();

// use asInstanceOf to preserve the current assertion info and description
return asInstanceOf(
new InstanceOfAssertFactory<>(
Either.Left.class, object -> new ObjectAssert<>(actual.getLeft())));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
* one or more contributor license agreements. See the NOTICE file distributed
* with this work for additional information regarding copyright ownership.
* Licensed under the Zeebe Community License 1.1. You may not use this file
* except in compliance with the Zeebe Community License 1.1.
*/
package io.camunda.zeebe.test.util.asserts;

import static org.assertj.core.api.Assertions.assertThatCode;

import io.camunda.zeebe.util.Either;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

@Execution(ExecutionMode.CONCURRENT)
final class EitherAssertTest {
@Nested
final class RightTest {
private final Either<Object, Object> actual = Either.right("right");
private final EitherAssert<Object, Object> assertion = EitherAssert.assertThat(actual);

@Test
void shouldFailIsLeftOnRight() {
assertThatCode(assertion::isLeft).isInstanceOf(AssertionError.class);
}

@Test
void shouldFailLeftOnRight() {
assertThatCode(assertion::left).isInstanceOf(AssertionError.class);
}

@Test
void isRightOnRight() {
assertion.isRight();
}

@Test
void shouldExtractRightOnRight() {
assertion.right().isEqualTo("right");
}
}

@Nested
final class LeftTest {
private final Either<Object, Object> actual = Either.left("left");
private final EitherAssert<Object, Object> assertion = EitherAssert.assertThat(actual);

@Test
void shouldFailIsRightOnLeft() {
assertThatCode(assertion::isRight).isInstanceOf(AssertionError.class);
}

@Test
void shouldFailRightOnLeft() {
assertThatCode(assertion::right).isInstanceOf(AssertionError.class);
}

@Test
void isLeftOnLeft() {
assertion.isLeft();
}

@Test
void shouldExtractLeftOnLeft() {
assertion.left().isEqualTo("left");
}
}

@Nested
final class FailOnNullTest {
private final EitherAssert<Object, Object> eitherAssert = EitherAssert.assertThat(null);

@ParameterizedTest(name = "{0}")
@MethodSource("assertions")
void shouldFailOnNull(
@SuppressWarnings("unused") final String testName,
final Consumer<EitherAssert<Object, Object>> assertion) {
assertThatCode(() -> assertion.accept(eitherAssert)).isInstanceOf(AssertionError.class);
}

private static Stream<Arguments> assertions() {
return Stream.of(
of("isLeft", EitherAssert::isLeft),
of("isRight", EitherAssert::isRight),
of("left", EitherAssert::left),
of("right", EitherAssert::right));
}

/** Convenience method to infer the type of the consumer in the test cases above. */
private static Arguments of(
final String testName, final Consumer<EitherAssert<Object, Object>> assertion) {
return Arguments.of(testName, assertion);
}
}
}
18 changes: 18 additions & 0 deletions util/src/main/java/io/camunda/zeebe/util/Either.java
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,14 @@ Collector<Either<L, R>, Tuple<List<L>, List<R>>, Either<List<L>, List<R>>> colle
*/
R get();

/**
* Returns the right value, or a default value if this is a {@link Left}.
*
* @param defaultValue the default value
* @return the right value, or the default value if this is a {@link Left}
*/
R getOrElse(R defaultValue);

/**
* Returns the left value, if this is a {@link Left}.
*
Expand Down Expand Up @@ -332,6 +340,11 @@ public R get() {
return value;
}

@Override
public R getOrElse(final R defaultValue) {
return value;
}

@Override
public L getLeft() {
throw new NoSuchElementException("Expected a left, but this is right");
Expand Down Expand Up @@ -421,6 +434,11 @@ public R get() {
throw new NoSuchElementException("Expected a right, but this is left");
}

@Override
public R getOrElse(final R defaultValue) {
return defaultValue;
}

@Override
public L getLeft() {
return value;
Expand Down
Loading

0 comments on commit 5ec45b8

Please sign in to comment.