001/* 002 * Copyright 2023 the original author or authors. 003 * <p> 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * <p> 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * <p> 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package de.cuioss.test.valueobjects.contract; 017 018import static de.cuioss.tools.collect.CollectionLiterals.mutableList; 019import static java.util.Objects.requireNonNull; 020 021import java.util.List; 022import java.util.Optional; 023 024import de.cuioss.test.valueobjects.api.TestContract; 025import de.cuioss.test.valueobjects.api.contracts.VerifyBuilder; 026import de.cuioss.test.valueobjects.objects.BuilderInstantiator; 027import de.cuioss.test.valueobjects.objects.ParameterizedInstantiator; 028import de.cuioss.test.valueobjects.objects.RuntimeProperties; 029import de.cuioss.test.valueobjects.objects.impl.BuilderConstructorBasedInstantiator; 030import de.cuioss.test.valueobjects.objects.impl.BuilderFactoryBasedInstantiator; 031import de.cuioss.test.valueobjects.objects.impl.BuilderParameterizedInstantiator; 032import de.cuioss.test.valueobjects.property.PropertyMetadata; 033import de.cuioss.test.valueobjects.property.PropertySupport; 034import de.cuioss.test.valueobjects.util.AnnotationHelper; 035import de.cuioss.tools.logging.CuiLogger; 036import de.cuioss.tools.reflect.MoreReflection; 037 038/** 039 * Defines basic tests for builder. In essence it will try to create a builder 040 * with a minimal set (required only) and one with all properties set. It sets 041 * the properties, build the actual object and verifies whether the properties 042 * are set correctly 043 * 044 * @author Oliver Wolff 045 * @param <T> identifying the type of objects to be tested 046 */ 047public class BuilderContractImpl<T> implements TestContract<T> { 048 049 private static final CuiLogger log = new CuiLogger(BuilderContractImpl.class); 050 051 private final BuilderInstantiator<T> builderInstantiator; 052 053 private final RuntimeProperties runtimeProperties; 054 055 /** The usually chosen name for the actual build method. */ 056 public static final String DEFAULT_BUILD_METHOD_NAME = "build"; 057 058 /** The usually chosen name for a factory builder method. */ 059 public static final String DEFAULT_BUILDER_FACTORY_METHOD_NAME = "builder"; 060 061 /** 062 * @param instantiator must not be null 063 * @param runtimeProperties must not be null 064 */ 065 public BuilderContractImpl(final BuilderInstantiator<T> instantiator, final RuntimeProperties runtimeProperties) { 066 builderInstantiator = requireNonNull(instantiator, "builderInstantiator must not be null"); 067 this.runtimeProperties = requireNonNull(runtimeProperties, "runtimeProperties must not be null."); 068 } 069 070 @Override 071 public void assertContract() { 072 073 final var builder = new StringBuilder("Verifying "); 074 builder.append(getClass().getName()).append("\nWith configuration: ").append(builderInstantiator.toString()); 075 log.info(builder.toString()); 076 setAndVerifyProperties(runtimeProperties.getRequiredProperties()); 077 setAndVerifyProperties(runtimeProperties.getAllProperties()); 078 shouldFailOnMissingRequiredAttributes(); 079 } 080 081 private void setAndVerifyProperties(final List<PropertyMetadata> propertiesToBeChecked) { 082 final var properties = propertiesToBeChecked.stream().map(PropertySupport::new).toList(); 083 084 properties.forEach(PropertySupport::generateTestValue); 085 086 final var builder = builderInstantiator.newBuilderInstance(); 087 for (final PropertySupport support : properties) { 088 support.apply(builder); 089 } 090 final var built = builderInstantiator.build(builder); 091 092 for (final PropertySupport support : properties) { 093 if (support.isReadable()) { 094 support.assertValueSet(built); 095 } 096 } 097 } 098 099 private void shouldFailOnMissingRequiredAttributes() { 100 if (runtimeProperties.getRequiredProperties().isEmpty()) { 101 return; 102 } 103 for (final PropertyMetadata property : runtimeProperties.getRequiredProperties()) { 104 final List<PropertyMetadata> requiredMinusOne = mutableList(runtimeProperties.getRequiredProperties()); 105 requiredMinusOne.remove(property); 106 var failed = false; 107 try { 108 setAndVerifyProperties(requiredMinusOne); 109 failed = true; 110 } catch (final AssertionError e) { 111 // Expected: Should have been thrown 112 } 113 if (failed) { 114 throw new AssertionError("Property is marked as required but the builder accepts if it is missing: " 115 + property.toString()); 116 } 117 } 118 } 119 120 @Override 121 public ParameterizedInstantiator<T> getInstantiator() { 122 return new BuilderParameterizedInstantiator<>(builderInstantiator, runtimeProperties); 123 } 124 125 /** 126 * Factory method for creating an instance of {@link BuilderContractImpl} 127 * depending on the given parameter 128 * 129 * @param beanType identifying the type to be tested. Must not be 130 * null 131 * @param annotated the annotated unit-test-class. It is expected 132 * to be annotated with {@link VerifyBuilder}, 133 * otherwise the method will return 134 * {@link Optional#empty()} 135 * @param initialPropertyMetadata identifying the complete set of 136 * {@link PropertyMetadata}, where the actual 137 * {@link PropertyMetadata} for the bean tests 138 * will be filtered by using the attributes 139 * defined within {@link VerifyBuilder}. Must not 140 * be null. 141 * @return an instance Of {@link BeanPropertyContractImpl} in case all 142 * requirements for the parameters are correct, otherwise it will return 143 * {@link Optional#empty()} 144 */ 145 public static final <T> Optional<BuilderContractImpl<T>> createBuilderTestContract(final Class<T> beanType, 146 final Class<?> annotated, final List<PropertyMetadata> initialPropertyMetadata) { 147 148 requireNonNull(beanType, "beantype must not be null"); 149 requireNonNull(annotated, "annotated must not be null"); 150 requireNonNull(initialPropertyMetadata, "initialPropertyMetadata must not be null"); 151 152 final Optional<VerifyBuilder> config = MoreReflection.extractAnnotation(annotated, VerifyBuilder.class); 153 154 if (config.isEmpty()) { 155 log.debug("No annotation of type BuilderTestContract available on class: " + annotated); 156 return Optional.empty(); 157 } 158 final var metadata = AnnotationHelper.handleMetadataForBuilderTest(annotated, initialPropertyMetadata); 159 160 final var contract = config.get(); 161 BuilderInstantiator<T> instantiator; 162 if (VerifyBuilder.class.equals(contract.builderClass())) { 163 if (VerifyBuilder.class.equals(contract.builderFactoryProvidingClass())) { 164 instantiator = new BuilderFactoryBasedInstantiator<>(beanType, contract.builderFactoryMethodName(), 165 contract.builderMethodName()); 166 } else { 167 instantiator = new BuilderFactoryBasedInstantiator<>(contract.builderFactoryProvidingClass(), 168 contract.builderFactoryMethodName(), contract.builderMethodName()); 169 } 170 } else { 171 instantiator = new BuilderConstructorBasedInstantiator<>(contract.builderClass(), 172 contract.builderMethodName()); 173 } 174 175 return Optional.of(new BuilderContractImpl<>(instantiator, new RuntimeProperties(metadata))); 176 } 177}