Skip to content

Commit

Permalink
Add hasRecordComponents to Class assertions (#2995)
Browse files Browse the repository at this point in the history
  • Loading branch information
ljrmorgan committed Apr 5, 2023
1 parent 5586451 commit 44c18fe
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@
import static org.assertj.core.error.ShouldBeAssignableTo.shouldBeAssignableTo;
import static org.assertj.core.error.ShouldBeRecord.shouldBeRecord;
import static org.assertj.core.error.ShouldBeRecord.shouldNotBeRecord;
import static org.assertj.core.error.ShouldHaveRecordComponents.shouldHaveRecordComponents;
import static org.assertj.core.error.ShouldNotBeNull.shouldNotBeNull;
import static org.assertj.core.util.Arrays.array;
import static org.assertj.core.util.Sets.newLinkedHashSet;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;

import org.assertj.core.internal.Classes;

Expand Down Expand Up @@ -277,6 +281,70 @@ private static boolean isRecord(Class<?> actual) {
}
}

/**
* Verifies that the actual {@code Class} has the given record components
* <p>
* Example:
* <pre><code class='java'> public class NotARecord {}
* public record MyRecord(String recordComponentOne, String recordComponentTwo) {}
*
* // these assertions succeed:
* assertThat(MyRecord.class).hasRecordComponents("recordComponentOne");
* assertThat(MyRecord.class).hasRecordComponents("recordComponentOne", "recordComponentTwo");
*
* // these assertions fail:
* assertThat(NotARecord.class).hasRecordComponents("recordComponentOne");
* assertThat(MyRecord.class).hasRecordComponents("recordComponentOne", "unknownRecordComponent");</code></pre>
*
* @param first the first record component name which must be in this class
* @param rest the remaining record component names which must be in this class
* @return {@code this} assertions object
* @throws AssertionError if {@code actual} is {@code null}.
* @throws AssertionError if the actual {@code Class} is not a record.
* @throws AssertionError if the actual {@code Class} doesn't contain all the record component names.
*
* @since 3.25.0
*/
public SELF hasRecordComponents(String first, String... rest) {
isRecord();
assertHasRecordComponents(first, rest);
return myself;
}

private void assertHasRecordComponents(String first, String[] rest) {
Set<String> expectedRecordComponents = newLinkedHashSet();
expectedRecordComponents.add(first);
if (rest != null) {
Collections.addAll(expectedRecordComponents, rest);
}
Set<String> missingRecordComponents = newLinkedHashSet();
Set<String> actualRecordComponents = getRecordComponentNames(this.actual);

for (String name : expectedRecordComponents) {
if (!actualRecordComponents.contains(name)) {
missingRecordComponents.add(name);
}
}
if (!missingRecordComponents.isEmpty()) {
throw assertionError(shouldHaveRecordComponents(this.actual, expectedRecordComponents, missingRecordComponents));
}
}

private static Set<String> getRecordComponentNames(Class<?> actual) {
try {
Method getRecordComponents = Class.class.getMethod("getRecordComponents");
Object[] recordComponents = (Object[]) getRecordComponents.invoke(actual);
Set<String> recordComponentNames = newLinkedHashSet();
for (Object recordComponent : recordComponents) {
Method getName = recordComponent.getClass().getMethod("getName");
recordComponentNames.add((String) getName.invoke(recordComponent));
}
return recordComponentNames;
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(e);
}
}

/**
* Verifies that the actual {@code Class} is final (has {@code final} modifier).
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* Copyright 2012-2023 the original author or authors.
*/
package org.assertj.core.error;

import java.util.Set;

/**
* Creates an error message indicating that an assertion that verifies that a class has record components failed.
*
* @author Louis Morgan
*/
public class ShouldHaveRecordComponents extends BasicErrorMessageFactory {

/**
* Creates a new <code>{@link ShouldHaveRecordComponents}</code>
*
* @param actual the actual value in the failed assertion.
* @param expected expected record components for this class
* @param missing missing record components for this class
* @return the created {@code ErrorMessageFactory}.
*/
public static ErrorMessageFactory shouldHaveRecordComponents(Class<?> actual, Set<String> expected, Set<String> missing) {
return new ShouldHaveRecordComponents(actual, expected, missing);
}

private ShouldHaveRecordComponents(Class<?> actual, Set<String> expected, Set<String> missing) {
super("%nExpecting%n" +
" %s%n" +
"to have the following record components:%n" +
" %s%n" +
"but it doesn't have:%n" +
" %s",
actual, expected, missing);
}

}
2 changes: 1 addition & 1 deletion assertj-core/src/main/java/org/assertj/core/util/Sets.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
public final class Sets {

/**
* Creates a <em>mutable</em> {@link HashSet} containing the given elements.
* Creates a <em>mutable</em> {@link LinkedHashSet} containing the given elements.
*
* @param <T> the generic type of the {@code HashSet} to create.
* @param elements the elements to store in the {@code HashSet}.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* Copyright 2012-2023 the original author or authors.
*/
package org.assertj.core.api.classes;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.BDDAssertions.then;
import static org.assertj.core.error.ShouldBeRecord.shouldBeRecord;
import static org.assertj.core.error.ShouldNotBeNull.shouldNotBeNull;
import static org.assertj.core.util.AssertionsUtil.expectAssertionError;

import java.util.ArrayList;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

/**
* @author Louis Morgan
*/
class ClassAssert_hasRecordComponents_Test {

@Test
void should_fail_if_actual_is_null() {
// GIVEN
Class<?> actual = null;
// WHEN
AssertionError error = expectAssertionError(() -> assertThat(actual).hasRecordComponents("component"));
// THEN
then(error).hasMessage(shouldNotBeNull().create());
}

@ParameterizedTest
@ValueSource(classes = {
String.class,
ArrayList.class,
ValueSource.class
})
void should_fail_if_actual_is_not_a_record(Class<?> actual) {
// WHEN
AssertionError error = expectAssertionError(() -> assertThat(actual).hasRecordComponents("component"));
// THEN
then(error).hasMessage(shouldBeRecord(actual).create());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ void should_fail_if_actual_is_null() {
// GIVEN
Class<?> actual = null;
// WHEN
AssertionError assertionError = expectAssertionError(() -> assertThat(actual).isNotRecord());
AssertionError error = expectAssertionError(() -> assertThat(actual).isNotRecord());
// THEN
then(assertionError).hasMessage(shouldNotBeNull().create());
then(error).hasMessage(shouldNotBeNull().create());
}

@ParameterizedTest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ void should_fail_if_actual_is_null() {
// GIVEN
Class<?> actual = null;
// WHEN
AssertionError assertionError = expectAssertionError(() -> assertThat(actual).isRecord());
AssertionError error = expectAssertionError(() -> assertThat(actual).isRecord());
// THEN
then(assertionError).hasMessage(shouldNotBeNull().create());
then(error).hasMessage(shouldNotBeNull().create());
}

@ParameterizedTest
Expand All @@ -47,9 +47,9 @@ void should_fail_if_actual_is_null() {
})
void should_fail_if_actual_is_not_a_record(Class<?> actual) {
// WHEN
AssertionError assertionError = expectAssertionError(() -> assertThat(actual).isRecord());
AssertionError error = expectAssertionError(() -> assertThat(actual).isRecord());
// THEN
then(assertionError).hasMessage(shouldBeRecord(actual).create());
then(error).hasMessage(shouldBeRecord(actual).create());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.assertj.core.api.BDDAssertions.then;
import static org.assertj.core.error.ShouldBeRecord.shouldNotBeRecord;
import static org.assertj.core.error.ShouldHaveRecordComponents.shouldHaveRecordComponents;
import static org.assertj.core.util.Sets.set;

import java.util.Set;

import org.junit.jupiter.api.Test;

Expand All @@ -25,21 +29,44 @@
class Assertions_assertThat_with_Record_Test {

@Test
void is_record_should_pass_if_actual_is_a_record() {
void isRecord_should_pass_if_actual_is_a_record() {
// WHEN/THEN
assertThat(MyRecord.class).isRecord();
}

@Test
void is_not_record_should_fail_if_actual_is_a_record() {
void isNotRecord_should_fail_if_actual_is_a_record() {
// WHEN
Throwable thrown = catchThrowable(() -> assertThat(MyRecord.class).isNotRecord());
// THEN
then(thrown).isInstanceOf(AssertionError.class)
.hasMessage(shouldNotBeRecord(MyRecord.class).create());
}

record MyRecord(String value) {
@Test
void hasRecordComponents_should_pass_if_record_has_expected_component() {
// WHEN/THEN
assertThat(MyRecord.class).hasRecordComponents("componentOne");
}

@Test
void hasRecordComponents_should_pass_if_record_has_expected_components() {
// WHEN/THEN
assertThat(MyRecord.class).hasRecordComponents("componentOne", "componentTwo");
}

@Test
void hasRecordComponents_should_fail_if_record_components_are_missing() {
// WHEN
Throwable thrown = catchThrowable(() -> assertThat(MyRecord.class).hasRecordComponents("componentOne", "missing"));
// THEN
then(thrown).isInstanceOf(AssertionError.class)
.hasMessage(shouldHaveRecordComponents(MyRecord.class,
set("componentOne", "missing"),
Set.of("missing")).create());
}

record MyRecord(String componentOne, String componentTwo) {
}

}

0 comments on commit 44c18fe

Please sign in to comment.