001/* 002 * MIT License 003 * 004 * Copyright (c) 2023 Michael Cowan 005 * 006 * Permission is hereby granted, free of charge, to any person obtaining a copy 007 * of this software and associated documentation files (the "Software"), to deal 008 * in the Software without restriction, including without limitation the rights 009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 010 * copies of the Software, and to permit persons to whom the Software is 011 * furnished to do so, subject to the following conditions: 012 * 013 * The above copyright notice and this permission notice shall be included in all 014 * copies or substantial portions of the Software. 015 * 016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 022 * SOFTWARE. 023 */ 024 025package io.blt.test.assertj; 026 027import java.lang.annotation.Annotation; 028import java.lang.reflect.Field; 029import java.lang.reflect.Method; 030import java.util.Arrays; 031import java.util.stream.Stream; 032import org.assertj.core.api.ObjectAssert; 033import org.assertj.core.internal.Failures; 034 035import static org.assertj.core.api.Assertions.assertThat; 036 037/** 038 * Assertion factory methods that allow testing the annotations of various types. 039 * <p> 040 * Each method is a static factory for an annotation specific assertion object 041 * i.e. each method returns {@code ObjectAssert<T extends Annotation>}. 042 * </p> 043 * e.g. 044 * <pre>{@code 045 * @Test 046 * void isAnnotatedAsTransactionalWithNoRollbackForException() { 047 * assertHasAnnotation(NotificationPublisher.class, Transactional.class) 048 * .extracting(Transactional::noRollbackFor) 049 * .isEqualTo(Exception.class); 050 * } 051 * }</pre> 052 */ 053public final class AnnotationAssertions { 054 055 private AnnotationAssertions() { 056 throw new IllegalAccessError("Utility class should be accessed statically and never constructed"); 057 } 058 059 /** 060 * Asserts that a {@code Method} is annotated with a given annotation. 061 * <p> 062 * If present, an assertion object is returned for the found annotation instance, else the test fails. 063 * </p> 064 * e.g. 065 * <pre>{@code 066 * @Test 067 * void isAnnotatedAsTransactionalWithNoRollbackForException() throws Exception { 068 * var method = NotificationPublisher.class.getMethod("sendNotification"); 069 * 070 * assertHasAnnotation(method, Transactional.class) 071 * .extracting(Transactional::noRollbackFor) 072 * .isEqualTo(Exception.class); 073 * } 074 * }</pre> 075 * 076 * @param method a {@code Method} to test for the presence of {@code annotation} 077 * @param annotation the expected {@code Annotation} type 078 * @param <T> type of {@code Annotation} 079 * @return an assertion object for the found annotation i.e. {@code ObjectAssert<T extends Annotation>} 080 */ 081 public static <T extends Annotation> ObjectAssert<T> assertHasAnnotation(Method method, Class<T> annotation) { 082 return assertThat(findAnnotationOfTypeOrFail(method.getAnnotations(), annotation)); 083 } 084 085 /** 086 * Asserts that a {@code Class} is annotated with a given annotation. 087 * <p> 088 * If present, an assertion object is returned for the found annotation instance, else the test fails. 089 * </p> 090 * e.g. 091 * <pre>{@code 092 * @Test 093 * void isAnnotatedAsTransactionalWithNoRollbackForException() { 094 * assertHasAnnotation(NotificationPublisher.class, Transactional.class) 095 * .extracting(Transactional::noRollbackFor) 096 * .isEqualTo(Exception.class); 097 * } 098 * }</pre> 099 * 100 * @param clazz a {@code Class} to test for the presence of {@code annotation} 101 * @param annotation the expected {@code Annotation} type 102 * @param <T> type of {@code Annotation} 103 * @return an assertion object for the found annotation i.e. {@code ObjectAssert<T extends Annotation>} 104 */ 105 public static <T extends Annotation> ObjectAssert<T> assertHasAnnotation(Class<?> clazz, Class<T> annotation) { 106 return assertThat(findAnnotationOfTypeOrFail(clazz.getAnnotations(), annotation)); 107 } 108 109 /** 110 * Asserts that a {@code Field} is annotated with a given annotation. 111 * <p> 112 * If present, an assertion object is returned for the found annotation instance, else the test fails. 113 * </p> 114 * e.g. 115 * <pre>{@code 116 * @Test 117 * void isAnnotatedAsDigitsWithFractionOfTwo() { 118 * var method = Invoice.class.getField("price"); 119 * 120 * assertHasAnnotation(field, Digits.class) 121 * .extracting(Digits::fraction) 122 * .isEqualTo(2); 123 * } 124 * }</pre> 125 * 126 * @param field a {@code Field} to test for the presence of {@code annotation} 127 * @param annotation the expected {@code Annotation} type 128 * @param <T> type of {@code Annotation} 129 * @return an assertion object for the found annotation i.e. {@code ObjectAssert<T extends Annotation>} 130 */ 131 public static <T extends Annotation> ObjectAssert<T> assertHasAnnotation(Field field, Class<T> annotation) { 132 var annotations = Stream.concat( 133 Arrays.stream(field.getAnnotations()), 134 Arrays.stream(field.getAnnotatedType().getAnnotations())) 135 .distinct() 136 .toArray(Annotation[]::new); 137 return assertThat(findAnnotationOfTypeOrFail(annotations, annotation)); 138 } 139 140 private static <T extends Annotation> T findAnnotationOfTypeOrFail(Annotation[] annotations, Class<T> type) { 141 var instance = Arrays.stream(annotations) 142 .filter(a -> a.annotationType().equals(type)) 143 .findFirst() 144 .orElseThrow(() -> Failures.instance().failure( 145 "Cannot find annotation of type " + type.getSimpleName())); 146 147 return type.cast(instance); 148 } 149 150}