Closed AngeloIglesias closed 1 year ago
Hi,
This is how Podam is desinged, type manufacturers are making the class, and then podam will go and try to fill it. From you post it is not so easy to understand what you are trying to achieve. It would help to see AssociationRuleDto and what exactly you are trying to customize there.
Thanks, Daniil
Hi, here is a short example, the class is auto-generated via OpenApi Generator:
import java.util.Objects;
import java.util.Arrays;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonTypeName;
/**
* AssociationRuleDto
*/
@JsonPropertyOrder({
AssociationRuleDto.JSON_PROPERTY_ASSOCIATION_RULE_TYPE,
AssociationRuleDto.JSON_PROPERTY_AEND_CARDINALITY,
AssociationRuleDto.JSON_PROPERTY_ZEND_CARDINALITY
})
@JsonTypeName("AssociationRule")
@javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", date = "2023-07-26T18:08:28.668830200+02:00[Europe/Berlin]")
public class AssociationRuleDto {
/**
* Gets or Sets associationRuleType
*/
public enum AssociationRuleTypeEnum {
FRAME_CONTRACT("CONTRACT_TYPE_A"),
PARTNER_CONTRACT("CONTRACT_TYPE_B");
private String value;
AssociationRuleTypeEnum(String value) {
this.value = value;
}
@JsonValue
public String getValue() {
return value;
}
@Override
public String toString() {
return String.valueOf(value);
}
@JsonCreator
public static AssociationRuleTypeEnum fromValue(String value) {
for (AssociationRuleTypeEnum b : AssociationRuleTypeEnum.values()) {
if (b.value.equals(value)) {
return b;
}
}
throw new IllegalArgumentException("Unexpected value '" + value + "'");
}
}
public static final String JSON_PROPERTY_ASSOCIATION_RULE_TYPE = "associationRuleType";
private AssociationRuleTypeEnum associationRuleType;
/**
* Gets or Sets aendCardinality
*/
public enum AendCardinalityEnum {
ONE("ONE"),
ONE_OR_MORE("ONE_OR_MORE"),
ZERO_OR_MORE("ZERO_OR_MORE");
private String value;
AendCardinalityEnum(String value) {
this.value = value;
}
@JsonValue
public String getValue() {
return value;
}
@Override
public String toString() {
return String.valueOf(value);
}
@JsonCreator
public static AendCardinalityEnum fromValue(String value) {
for (AendCardinalityEnum b : AendCardinalityEnum.values()) {
if (b.value.equals(value)) {
return b;
}
}
throw new IllegalArgumentException("Unexpected value '" + value + "'");
}
}
public static final String JSON_PROPERTY_AEND_CARDINALITY = "aendCardinality";
private AendCardinalityEnum aendCardinality;
/**
* Gets or Sets zendCardinality
*/
public enum ZendCardinalityEnum {
ONE("ONE"),
ONE_OR_MORE("ONE_OR_MORE"),
ZERO_OR_MORE("ZERO_OR_MORE");
private String value;
ZendCardinalityEnum(String value) {
this.value = value;
}
@JsonValue
public String getValue() {
return value;
}
@Override
public String toString() {
return String.valueOf(value);
}
@JsonCreator
public static ZendCardinalityEnum fromValue(String value) {
for (ZendCardinalityEnum b : ZendCardinalityEnum.values()) {
if (b.value.equals(value)) {
return b;
}
}
throw new IllegalArgumentException("Unexpected value '" + value + "'");
}
}
public static final String JSON_PROPERTY_ZEND_CARDINALITY = "zendCardinality";
private ZendCardinalityEnum zendCardinality;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AssociationRuleDto associationRule = (AssociationRuleDto) o;
return Objects.equals(this.associationRuleType, associationRule.associationRuleType) &&
Objects.equals(this.aendCardinality, associationRule.aendCardinality) &&
Objects.equals(this.zendCardinality, associationRule.zendCardinality);
}
@Override
public int hashCode() {
return Objects.hash(associationRuleType, aendCardinality, zendCardinality);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("class AssociationRuleDto {\n");
sb.append(" associationRuleType: ").append(toIndentedString(associationRuleType)).append("\n");
sb.append(" aendCardinality: ").append(toIndentedString(aendCardinality)).append("\n");
sb.append(" zendCardinality: ").append(toIndentedString(zendCardinality)).append("\n");
sb.append("}");
return sb.toString();
}
/**
* Convert the given object to string with each line indented by 4 spaces
* (except the first line).
*/
private String toIndentedString(Object o) {
if (o == null) {
return "null";
}
return o.toString().replace("\n", "\n ");
}
}
When I do the following JUnit Test..
@Test
void testQueryContract()
throws Exception
{
ContractID contractID = new ContractID(Domain.XXX, 1);
ContractDto contractDto = podamFactory.manufacturePojo(ContractDto.class);
wireMockServer.stubFor(WireMock.get(
WireMock.urlEqualTo(QUERY_CONTRACT_PATH.replace("{id}", IdHelper.toString(contractID)))).willReturn(
TestHelper.jsonResponse().withBody(mapper.writeValueAsString(contractDto))));
Contract result = contractQueryWebClientAdapter.queryContract(contractID);
assertThat(result).isNotNull();
ContractAssertionHelper.assertContract(contractDto, result);
}
.. I run into an error because of the internal validation of AssociationRuleDto
: For example I can not have CONTRACT_TYPE_B
in one field and a ONE
to ONE_OR_MORE
relationship mapped onto the other fields of AssociationRuleDto. So the classic attribute Strategy of PODAM will fail. Maybe I could set constraints for each attribute / field with the aid of PODAM, but it would be extrem complicated to implement for a test case.
That is why I wanted PODAM to use a random instance AssociationRuleDto (i determine the possible instances) instead of getting the fields initilized with random values through PODAM. PODAM would still be very useful since I just have to mock the ContractDto and PODAM still "injects" a random instance of AssociationRuleDto. I could not find the case in the documentation.
But I would have expected at least the DataProviderStrategy to do this kind of stuff, since of the "Data" in its name. Adding every field separately is too complicated in case of existing Validation. So I am force, to use one instance of AssociationRuleDto to keep the attribution simple or overwrite the random values later. None of the solutions is quite elegant.. Or do I miss something?
I noticed there was a significant change in version 8, so it might now work with the AbstractTypeManufacturer
:
I cannot verify it because I'm still using Spring Boot 2 and encountering an issue with Mockito when I include jakarta.validation:jakarta.validation-api:3.0.2
.
This is just a refactoring, the behaviour has not changed. But I included your change with avoiding creating string twice.
Thanks, Daniil
Yes, I saw it, too. I meant your Commit "Encapsulate typeArgsMap in ManufacturingContext, TypeManufacturer int…":
However I could solve my Problem in Podam 7 this way:
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import uk.co.jemos.podam.api.AbstractRandomDataProviderStrategy;
import uk.co.jemos.podam.api.ClassAttribute;
import uk.co.jemos.podam.common.AttributeStrategy;
/**
* Customized variant of PODAM´s Data Provider to be able to define the used data at class level
* - Allows constraints between field values
* - Reflection could be used to copy existing classes automagically
*
* @author mfernandezig
* @since 01.08.2023
*/
public class CustomDataProviderStrategy extends AbstractRandomDataProviderStrategy
{
// Define separate data provider for each class/pojo:
private Map<Class<?>, Function<ClassAttribute, Object>> functionMap = new HashMap<>();
//Second provider method if no class level value was supplied, not required.
private Function<ClassAttribute, Object> hook;
/**
* New Instance of PODAM`s Data Provider Strategy, which can be injected in the PodamFactoryImpl (via constructor)
*
* @param provider A function map which defines for which class a custom strategy should be applied and
* which function serves as data provider for the determined class
*/
public CustomDataProviderStrategy(CustomDataProvider provider)
{
this.functionMap = provider.getFunctionMap(); //ToDo: May be generalized to add data of multiple maps not just one.
this.hook = provider.getHookValue(); // setze den globalen Hook aus dem provider (nicht erforderlich)
}
//~ Methods ----------------------------------------------------------------------------------------------------------------
@Override
public AttributeStrategy<?> getStrategyForAttribute(final ClassAttribute attribute)
{
AttributeStrategy<?> strategy = null;
strategy = customizeStrategy(attribute);
if(strategy == null)
{
strategy = super.getStrategyForAttribute(attribute); //run as normal (default)
}
return strategy;
}
/**
* Injects data for each class (the way) defined in the function map, if any
*
* @param attribute PODAM`s Field Descriptor
* @return PODAM`s special strategy to manipulate a single field (in podam called attribute) value
*/
private AttributeStrategy<?> customizeStrategy(ClassAttribute attribute) {
AttributeStrategy<?> strategy = null; // null determines default strategy
// Select and use correct data provider:
Object value = Optional.ofNullable(functionMap.get(attribute.getAttribute().getDeclaringClass()))
.map(f -> f.apply(attribute)) //Check more specific class level strategy first
.orElseGet(() -> hook != null ? hook.apply(attribute) : null); //else check global strategy if provided
// Create custom strategy if required:
if( value != null)
{
strategy = new AttributeStrategy<Object>() {
@Override
public Object getValue(Class<?> attrType, List<Annotation> attrAnnotations) {
return value;
}
};
}
return strategy;
}
}
import uk.co.jemos.podam.api.ClassAttribute;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.function.Function;
/**
*
* @author mfernandezig
* @since 01.08.2023
*/
public abstract class CustomDataProvider {
protected static final Random RANDOM = new Random();
protected static final Map<Class<?>, Function<ClassAttribute, Object>> functionMap = new HashMap<>();
/**
* Get map for class level resolution
*
* @return
*/
Map<Class<?>, Function<ClassAttribute, Object>> getFunctionMap() {
return functionMap;
}
/**
* Get global hook if set
*
* @return
*/
public Function<ClassAttribute, Object> getHookValue() {
return attribute -> null; // standardmäßig immer null, außer in der abgeleiteten Klasse überschrieben
}
/**
* Returns the field name of class in lower case letters
* Used for jump table (switch statement)
*
* @param attribute Podam class with field metadata
* @return
*/
protected static String nameOf(ClassAttribute attribute)
{
return attribute.getName().toLowerCase(); // case-sensitivity vermeiden
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// UTIL
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
protected static OffsetDateTime randomOffsetDateTime(String startStr, String endStr) {
LocalDateTime start = LocalDateTime.parse(startStr);
LocalDateTime end = LocalDateTime.parse(endStr);
long startSeconds = start.toEpochSecond(ZoneOffset.UTC);
long endSeconds = end.toEpochSecond(ZoneOffset.UTC);
long random = startSeconds + (long)(RANDOM.nextDouble() * (endSeconds - startSeconds));
return OffsetDateTime.ofInstant(Instant.ofEpochSecond(random), ZoneOffset.UTC);
}
/**
* Generates a random enum value
*
* @param enumClass The enum which values should be used
* @return The String of a random enum value
*/
protected String generateRandomEnumValueAsString(Class<? extends Enum<?>> enumClass)
{
Object[] enumValues = enumClass.getEnumConstants();
int randomIndex = RANDOM.nextInt(enumValues.length);
return enumValues[randomIndex].toString();
}
}
Configuring Podam Factory via Spring:
@Configuration
public class PodamConfig
{
@Bean
@Profile("Query")
public PodamFactory queryPodamFactory() {
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
//Allow custom class creation via global strategy:
DataProviderStrategy strategy = new CustomDataProviderStrategy(new QueryData());
PodamFactory factory = new PodamFactoryImpl(strategy);
//Customized podam manufacturers:
CustomIntegerManufacturer customIntegerManufacturer = new CustomIntegerManufacturer();
factory.getStrategy().addOrReplaceTypeManufacturer(Integer.class, customIntegerManufacturer);
return factory;
}
}
Setting up some test data for example:
import /*package name*/.podam.strategies.CustomDataProvider;
import uk.co.jemos.podam.api.ClassAttribute;
public class QueryData extends CustomDataProvider {
private static final int ASSOCIATION_RULE_VARIANT = RANDOM.nextInt(4);
private static int serviceCounter = 0;
private static final AssociationRuleDto[] ASSOCIATION_RULE_VARIANTS = generateAssociationRules();
public QueryData()
{
functionMap.put(AssociationRuleDto.class, QueryData::associationRule);
functionMap.put(ServiceDto.class, QueryData::service);
}
public static Object associationRule(ClassAttribute field) {
// Match exactly:
switch (nameOf(field))
{
case "aendcardinality":
return ASSOCIATION_RULE_VARIANTS[ASSOCIATION_RULE_VARIANT].getAendCardinality();
case "zendcardinality":
return ASSOCIATION_RULE_VARIANTS[ASSOCIATION_RULE_VARIANT].getZendCardinality();
default:
return null;
}
}
public static Object service(ClassAttribute field) {
// Match exactly:
switch (nameOf(field))
{
case "serviceid":
return "Service No. " + (999 + serviceCounter++);
default:
return null;
}
}
@Override
public Function<ClassAttribute, Object> getHookValue() {
// Replace Regex in all places:
return field -> {
Object value = null;
if ((nameOf(field).contains("contractid")))
{
value = "1234";
}
return value;
};
}
////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
private static AssociationRuleDto[] generateAssociationRules() {
AssociationRuleDto[] data = new AssociationRuleDto[4];
for( int i = 0; i < 4; i++)
{
data[i] = new AssociationRuleDto();
switch (i)
{
case 0:
data[i].setAendCardinality(AssociationRuleDto.AendCardinalityEnum.ONE);
data[i].setZendCardinality(AssociationRuleDto.ZendCardinalityEnum.ZERO_OR_MORE);
break;
case 1:
data[i].setAendCardinality(AssociationRuleDto.AendCardinalityEnum.ONE);
data[i].setZendCardinality(AssociationRuleDto.ZendCardinalityEnum.ONE_OR_MORE);
break;
case 2:
data[i].setAendCardinality(AssociationRuleDto.AendCardinalityEnum.ONE);
data[i].setZendCardinality(AssociationRuleDto.ZendCardinalityEnum.ONE_OR_MORE);
break;
case 3:
data[i].setAendCardinality(AssociationRuleDto.AendCardinalityEnum.ONE);
data[i].setZendCardinality(AssociationRuleDto.ZendCardinalityEnum.ONE);
break;
}
}
return data;
}
}
Setting up a test example:
@ContextConfiguration(initializers = WireMockInitializer.class)
@Import(WebClientConfiguration.class)
@SpringBootTest
@ActiveProfiles("Query")
public class ServiceQueryWebClientAdapterTest
{
private static final String QUERY_PATH = "/query/{id}";
private final WireMockServer wireMockServer = WireMockSingleton.getServer();
@Autowired
private ContractQueryRequestMapper contractQueryRequestMapper;
@Autowired
private ContractQueryService contractQueryWebClientAdapter;
@Autowired
private ObjectMapper mapper;
@Autowired
private PodamFactory podamFactory;
@BeforeEach
public void beforeEach()
{
assertThat(podamFactory).withFailMessage("The PODAM factory cannot be null!").isNotNull();
assertThat(podamFactory.getStrategy()).withFailMessage("The factory strategy cannot be null!").isNotNull();
wireMockServer.resetAll();
}
@Test
void testQuery()
throws Exception
{
String serviceID = new Service(/*..*/)
ServiceDto serviceDto = podamFactory.manufacturePojo(ServiceDto.class);
wireMockServer.stubFor(WireMock.get(
WireMock.urlEqualTo(QUERY_PATH.replace("{id}", serviceID))).willReturn(
TestHelper.jsonResponse().withBody(mapper.writeValueAsString(serviceDto))));
Service service = contractQueryWebClientAdapter.queryContract(serviceID);
assertThat(serviceID).isNotNull();
ContractAssertionHelper.assertService(serviceDto, service);
}
}
So far I found out that there are two ways to let PODAM choose a class instance (not just some fields aka. attributes) as a whole.
DataProviderStrategy
TypeManufacturers
(Custom IntTypeManufacturerImpl or IntTypeManufacturerImplare both working fine)I debugged both versions, but they have the same problem: Both versions are called correctly, but PODAM does not memorize that the instances have been already initialized and initializes every field again with a random value. I can not see any differences to the
StringTypeManufacturerImpl
or theIntTypeManufacturerImpl
and why they are working.I guess an Issue with
ManufacturingContext
in thePodamFactoryImpl
or maybe it can be fixed with the ClassStrategy: