spring-cloud / spring-cloud-contract

Support for Consumer Driven Contracts in Spring
https://cloud.spring.io/spring-cloud-contract
Apache License 2.0
720 stars 439 forks source link

How to enforce an enum in a contract #451

Closed DavidOpDeBeeck closed 6 years ago

DavidOpDeBeeck commented 7 years ago

Problem

A contract definition currently does not provide a way to easily enforce the existence of all the values of an enum at the producer side. This is necessary, because it is important that the enum values remain synchronized between the producer and the consumer.

Example

The example below defines an application that contains a producer that provides an API to create a person and a consumer that will consume this person creation API.

Application

Context

The application consists of 3 independent projects:

Class location

Producer

POST /api/person
package com.company.producer;

class PersonTO {
    private String name;
    private Gender gender;
}
package com.company.producer;

enum Gender {
    MALE, FEMALE, UNKNOWN
}

Contract

Regex

We can use a regex $(regex('(MALE|FEMALE|UNKNOWN)')) to allow for all the gender enum values, but only 1 will be selected at random for the producer test. This limitation doesn't enforce that all the enum values have to exist on the producer side.

package com.company.contracts

import org.springframework.cloud.contract.spec.Contract

Contract.make {
    request {
        method 'POST'
        url '/api/person'
        body([
               "name": "David",
               "gender": $(regex('(MALE|FEMALE|UNKNOWN)'))
        ])
        headers {
            contentType('application/json')
        }
    }
    response {
        status 200
    }
}

Multiple definitions

We could automate the creation of the contract above by creating a contract for each enum value. This ensures that the producer supports all the gender enum values. The disadvantage of this approach is that this becomes a mess when working with multiple enums in the same request body.

package com.company.contracts.constants;

enum Gender {
    MALE, FEMALE, UNKNOWN
}
package com.company.contracts

import org.springframework.cloud.contract.spec.Contract
import com.company.contracts.constants.Gender

import java.util.stream.Collectors
import java.util.stream.Stream

static Contract makeContract(gender) {
    Contract.make {
        request {
            method 'POST'
            url '/api/person'
            body([
                "name": "David",
                "gender": gender.name()
            ])
            headers {
                contentType('application/json')
            }
        }
        response {
            status 200
        }
    }
}

Stream.of(Gender.values())
        .map(this.&makeContract)
        .collect(Collectors.toSet())

Do it yourself

We could also solve this problem by writing our own tests comparing the enum values with the enum values from the contracts.

Producer

package com.company.producer;

class EnumValuesTest {

    @Test
    public void gender() {
        assertThat(Gender.class)
            .hasSameValuesAs(com.company.contracts.constants.Gender.class);
    }
}

Consumer

package com.company.consumer;

class EnumValuesTest {

    @Test
    public void gender() {
        assertThat(Gender.class)
            .hasSameValuesAs(com.company.contracts.constants.Gender.class);
    }
}

Conclusion

Maybe I'm missing some feature that Spring Cloud Contract provides to solve this problem, because I'm not entirely happy with any of the above solution.

marcingrzejszczak commented 7 years ago

We had a chat with @dsyer and we came to the conclusion that you should go with the Multiple definitions approach.

If that's not fully satisfactory you can turn off the default JAR generation (https://github.com/spring-cloud-samples/spring-cloud-contract-samples/blob/master/producer_with_restdocs/pom.xml#L30) and generate a JAR with stubs yourself (https://github.com/spring-cloud-samples/spring-cloud-contract-samples/blob/master/producer_with_restdocs/pom.xml#L102-L119). You can add the POJOs to that stub (https://github.com/spring-cloud-samples/spring-cloud-contract-samples/blob/master/producer_with_restdocs/src/assembly/stub.xml#L11-L24). That way you can then add the stub as a test dependency you will have the POJO on the consumer / producer side (https://github.com/spring-cloud-samples/spring-cloud-contract-samples/blob/master/consumer_with_restdocs/pom.xml#L46-L58).

Does it make sense? If you have any ideas how to improve this process then we'll be very happy with the feedback.

DavidOpDeBeeck commented 7 years ago

Thanks for the quick response!

If I understand your second paragraph correctly you propose a shared library between the producer, consumer and contracts projects? This library would then contain all the shared classes (in the example above the Gender.java class)?

We specifically chose not to create such shared library, one of the reasons was that the maintainability of this library would increase and cause problems in the long run. This decision was made by taking the usage of the consumer service and previous experiences into account.

To get back to the Multiple definitions approach. My opinion would be that this behavior should be supported / implemented by the plugin (without manually looping over all the enum values).

marcingrzejszczak commented 6 years ago

Making such a feature in the plugin would be complex. We would have to analyze the body (which we already do) and check if there are enums there. Then, we would have to store this information and do a combination of possibilities. I'd prefer not to touch the JSON parsing functionality cause it's already very complex. This feature would make it unsupportable IMO.

I think that a good compromise would be to go with Multiple definitions. From the user perspective, it's trivial to make such a combination, cause the user knows which of the elements are enums.