pact

How to test contracts between classes exchanging dictionaries?


Our organization uses many classes which are maintained by different teams and exchange dictionary collections. We want to make sure that the provider team will be notified when they break a contract. Is this possible using pact?

One sample provider class:

import java.util.Dictionary;
import java.util.Hashtable;

public class MoneyTransfer {
    public Dictionary<String, Object> toIBAN(Dictionary<String, Object> input) 
    {
        
        double amount = (Double) input.get("amount");
        String iban = input.get("iban").toString();
        
        Double commission = doTransfer(iban, amount);
        
        Dictionary<String, Object> output = new Hashtable<String, Object>();
        output.put("commission", commission);
        
        return output;
        
    }

    private double doTransfer(String iban, double amount) {
        // TODO Auto-generated method stub
        return 0;
    }
}

So we want to be able to break a contract test when a developer makes this code change which will compile OK but break the consumers:

String iban = input.get("account no").toString();

Solution

  • We did this by writing an adapter REST service in front of the actual class accepting a dictionary:

    @RequestMapping(
            value="/CALL_DICTIONARY_SERVICE", 
            method = { RequestMethod.POST })
    public ResponseEntity<Object> callDictionaryService(@RequestBody Object request) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {
        
        Dictionary<String, Object> input = DictionaryRequestFactory.createDictionary(request);
        
        Object dictionaryService = locateDictionaryService(request);
        
        Dictionary<String, Object> output = callDictionaryService(dictionaryService, input);
        
        Object response = RestResponseFactory.createResponse(output);
    
        return new ResponseEntity<Object>(response, HttpStatus.OK);
    }
    //Rest of the code omitted for simplicity..
    

    Consumer side constructs the contract using simple extension classes to Pact framework:

    @Pact(consumer="Money Transfer Consumer")
    public RequestResponsePact pactTransferToIBAN(PactDslWithProvider dsl) {
        
        Dictionary<String, Object> input = new Hashtable<>();
        input.put("amount", "123");
        input.put("iban", "TR000123456");
        
        Dictionary<String, Object> output = new Hashtable<>();
        output.put("commission", "12");
        
        DictionaryDslWithProvider builder = new DictionaryDslWithProvider(dsl);
        
        return builder
                .given("Sender account has enough money")
                .uponReceiving("TRANSFER_TO_IBAN")
                .dictionary(input)
              .willRespondWith()
                .dictionary(output)
              .toPact();
    }
    

    Extension classes convert the dictionary parameter to REST service parameters:

    public class DictionaryDslWithProvider {
    
        PactDslWithProvider baseDsl;
        
        public DictionaryDslWithProvider(PactDslWithProvider baseDsl) {
            this.baseDsl = baseDsl;
        }
        
        public DictionaryDslWithState given(String state) {
            PactDslWithState dsl = baseDsl.given(state);
            return new DictionaryDslWithState(dsl);
        }
    }
    public class DictionaryDslWithState {
    
        PactDslWithState baseDsl;
        
        public DictionaryDslWithState(PactDslWithState dsl) {
            baseDsl = dsl;
        }
    
        public DslRequestWithoutDictionary uponReceiving(String dictionaryServiceName) {
            return new DslRequestWithoutDictionary(baseDsl.uponReceiving(dictionaryServiceName)
                    .path("/CALL_DICTIONARY_SERVICE"
                    ,dictionaryServiceName
                    );
        }
    }
    public class DslRequestWithoutDictionary {
    
        PactDslRequestWithPath req;
        private String dictionaryServiceName;
        
        public DslRequestWithoutDictionary(PactDslRequestWithPath pactDslRequestWithPath, String dictionaryServiceName) {
            req = pactDslRequestWithPath;
            this.dictionaryServiceName = dictionaryServiceName;
        }
    
        public PactDslRequestWithDictionary dictionary(Dictionary<String, Object> dictionary) {
            
            String jsonBody = dictionaryToJsonBody(dictionary, dictionaryServiceName);
            
            return new PactDslRequestWithDictionary(
                    req
                        .method("POST")
                        .body(jsonBody)
                        );
        }
        //Rest of the code omitted for simplicity..
    }
    public class PactDslRequestWithDictionary {
    
        PactDslRequestWithPath body;
        
        public PactDslRequestWithDictionary(PactDslRequestWithPath body) {
            this.body = body;
        }
    
        public DictionaryDslResponse willRespondWith() {
            return new DictionaryDslResponse(body.willRespondWith());
        }
    }
    

    When the consumer creates the contract, here is what he gets:

    Method:POST
    Path:/CALL_DICTIONARY_SERVICE
    Body:
    {
        "amount": "123",
        "iban": "TR000123456",
        "_DICTIONARY_SERVIS_NAME_": "TRANSFER_TO_IBAN"
    }
    

    Provider uses the dictonary service name mentioned in the contract to find the correct method and calls it with the input converted from JSON to Dictionary.