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 org.assertj.core.api.ObjectAssert; 032import org.assertj.core.internal.Failures; 033 034import static org.assertj.core.api.Assertions.assertThat; 035 036/** 037 * Assertion factory methods that allow testing the annotations of various types. 038 * <p> 039 * Each method is a static factory for an annotation specific assertion object 040 * i.e. each method returns {@code ObjectAssert<T extends Annotation>}. 041 * </p> 042 * e.g. 043 * <pre>{@code 044 * @Test 045 * void isAnnotatedAsTransactionalWithNoRollbackForException() { 046 * assertHasAnnotation(NotificationPublisher.class, Transactional.class) 047 * .extracting(Transactional::noRollbackFor) 048 * .isEqualTo(Exception.class); 049 * } 050 * }</pre> 051 */ 052public final class AnnotationAssertions { 053 054 private AnnotationAssertions() { 055 throw new IllegalAccessError("Utility class should be accessed statically and never constructed"); 056 } 057 058 /** 059 * Asserts that a {@code Method} is annotated with a given annotation. 060 * <p> 061 * If present, an assertion object is returned for the found annotation instance, else the test fails. 062 * </p> 063 * e.g. 064 * <pre>{@code 065 * @Test 066 * void isAnnotatedAsTransactionalWithNoRollbackForException() throws Exception { 067 * var method = NotificationPublisher.class.getMethod("sendNotification"); 068 * 069 * assertHasAnnotation(method, Transactional.class) 070 * .extracting(Transactional::noRollbackFor) 071 * .isEqualTo(Exception.class); 072 * } 073 * }</pre> 074 * 075 * @param method a {@code Method} to test for the presence of {@code annotation} 076 * @param annotation the expected {@code Annotation} type 077 * @param <T> type of {@code Annotation} 078 * @return an assertion object for the found annotation i.e. {@code ObjectAssert<T extends Annotation>} 079 */ 080 public static <T extends Annotation> ObjectAssert<T> assertHasAnnotation(Method method, Class<T> annotation) { 081 return assertThat(findAnnotationOfTypeOrFail(method.getAnnotations(), annotation)); 082 } 083 084 /** 085 * Asserts that a {@code Class} is annotated with a given annotation. 086 * <p> 087 * If present, an assertion object is returned for the found annotation instance, else the test fails. 088 * </p> 089 * e.g. 090 * <pre>{@code 091 * @Test 092 * void isAnnotatedAsTransactionalWithNoRollbackForException() { 093 * assertHasAnnotation(NotificationPublisher.class, Transactional.class) 094 * .extracting(Transactional::noRollbackFor) 095 * .isEqualTo(Exception.class); 096 * } 097 * }</pre> 098 * 099 * @param clazz a {@code Class} to test for the presence of {@code annotation} 100 * @param annotation the expected {@code Annotation} type 101 * @param <T> type of {@code Annotation} 102 * @return an assertion object for the found annotation i.e. {@code ObjectAssert<T extends Annotation>} 103 */ 104 public static <T extends Annotation> ObjectAssert<T> assertHasAnnotation(Class<?> clazz, Class<T> annotation) { 105 return assertThat(findAnnotationOfTypeOrFail(clazz.getAnnotations(), annotation)); 106 } 107 108 /** 109 * Asserts that a {@code Field} is annotated with a given annotation. 110 * <p> 111 * If present, an assertion object is returned for the found annotation instance, else the test fails. 112 * </p> 113 * e.g. 114 * <pre>{@code 115 * @Test 116 * void isAnnotatedAsDigitsWithFractionOfTwo() { 117 * var method = Invoice.class.getField("price"); 118 * 119 * assertHasAnnotation(field, Digits.class) 120 * .extracting(Digits::fraction) 121 * .isEqualTo(2); 122 * } 123 * }</pre> 124 * 125 * @param field a {@code Field} to test for the presence of {@code annotation} 126 * @param annotation the expected {@code Annotation} type 127 * @param <T> type of {@code Annotation} 128 * @return an assertion object for the found annotation i.e. {@code ObjectAssert<T extends Annotation>} 129 */ 130 public static <T extends Annotation> ObjectAssert<T> assertHasAnnotation(Field field, Class<T> annotation) { 131 return assertThat(findAnnotationOfTypeOrFail(field.getAnnotations(), annotation)); 132 } 133 134 private static <T extends Annotation> T findAnnotationOfTypeOrFail(Annotation[] annotations, Class<T> type) { 135 var instance = Arrays.stream(annotations) 136 .filter(a -> a.annotationType().equals(type)) 137 .findFirst() 138 .orElseThrow(() -> Failures.instance().failure( 139 "Cannot find annotation of type " + type.getSimpleName())); 140 141 return type.cast(instance); 142 } 143 144}