InternationalTradeAdministration / developer-best-practices

3 stars 1 forks source link

Mocking/Stubbing Framework: extend FFLIB or roll our own #1

Open scottpelak opened 6 years ago

scottpelak commented 6 years ago

fflib_ApexMock has a potential bug: static String extractTypeName(Object mockInstance) only works if toString() isn't overridden

public static String extractTypeName(Object mockInstance)
{
    return String.valueOf(mockInstance).split(':').get(0);
}

An alternative is have an interface/abstract method on fflib_ApexMocks to return the Type of the implementation: Type getType() which can streamline stub creation.

What's even more fun is non-test classes can have an inline/hard-coded Type of a @IsTest class .

Below is a quick-draft sample Stubbing framework. One still has to manually code when to use the stub during a test. Even further below is a sample Mocking framework for APIs which doesn't require to manually use the Mock during a Test -- the API always sets the Mock when Test.isRunningTest() is true.

Stubbing Framework

public interface Typeable {
    Type getType();
}

public interface Stubbable extends Typeable {
    Type getStubType();
}

public with sharing abstract Stub
    implements 
        System.StubProvider, 
        Typeable
{
    public abstract Type getType();
    public abstract Type getStubbedType();
    public abstract Object handleMethodCall(
        Object stubbedObject, 
        String stubbedMethodName, 
        System.Type returnType, 
        System.Type[] parameterTypes, 
        String[] parameterNames, 
        Object[] parameterValues
    );

    public virtual void doOnCreateStub(Stubbable stubbable) {
        // Default: do nothing
    }

    public Object createStub() {
        return System.createStub(this.getStubbedType(), this);
    }

    public with sharing class NotStubbableException extends Exception {}

    public static Stub createStub(Stubbable stubbable) {
        final Type stubType = stubbable == null ? null : stubbable.getStubType();
        final Object value = stubType == null ? null : stubType.newInstance();
        Stub stub;
        if(value instanceof Stub) {
            stub = (Stub) value;
            stub.doOnCreate(stubbable);
        }
        else {
            throw NotStubbableException (String.format(
                System.Label.Stub_NotStubbableException, 
                new String[] {
                    // Exception Message parameters
                }
            ));
        }
        return stub;
    }
}

@IsTest
public with sharing ProductionClass_Stub extends Stub {
    private ProductionClass productionClass;

    // Must have an empty constructor
    public ProductionClass() {
        // foo-bar
    }

    public override Type getType() {
        return ProductionClass_Stub.class;
    }

    public override Type getStubbedType() {
         return ProductionClass.class;
    }

    public abstract Object handleMethodCall(
        Object stubbedObject, 
        String stubbedMethodName, 
        System.Type returnType, 
        System.Type[] parameterTypes, 
        String[] parameterNames, 
        Object[] parameterValues
    ) {
       Object response;
       {
             // Set response.  Maybe use this.productionClass...
       }
       return response;
    }

    public override void doOnCreateStub(Stubbable stubbable) {
        if(stubbable instanceof ProductionClass) {
            this.productionClass = (ProductionClass) stubbable;
        }
    }
}

public with sharing ProductionClass implements Stubbable {
    public Type getType() {
        return ProductionClass.class;
    }

    public Type getStubType() {
        return ProductionClass_Stub.class;  // Legal to reference @IsTest Type even in non-@IsTest class!
    }
}

public with sharing AnotherClassUsingProductionClass {
    @TestVisible private ProductionClass productionClass;

    public ProductionClass getProductionClass() {
        return this.productionClass;
    }

    public void setProductionClass(ProductionClass productionClass) {
        this.productionClass = productionClass;
    }
}

@IsTest
public with sharing class AnotherClassUsingProductionClass_Test {

    @IsTest
    public static void testAnotherClassUsingProductionClass() {
        final ProductionClass[] productionClasses = new ProductionClass[] {};
        // Create stubbed instance directly from Stub
        {
            final ProductionClass_Stub stub = new ProductionClass_Stub();
            {
                // Set stub
            }
            productionClasses.add((ProductionClass) stub.createStub());
        }

        // Create stubbed instance from an instance
        {
            final ProductionClass instance = new ProductionClass();
            {
                 // Set instance
            }
            productionClasses .add((ProductionClass) Stub.createStub(instance));
        }

        Test.startTest();

        // AnotherClassUsingProductionClass 
        for(ProductionClass productionClass : productionClasses) {
             final AnotherClassUsingProductionClass anotherClassUsingProductionClass = new AnotherClassUsingProductionClass();

             // void setProductionClass(ProductionClass productionClass)
            {
                anotherClassUsingProductionClass.productionClass = null;

                 Exception e;

                 try { 
                      anotherClassUsingProductionClass.setProductionClass(productionClass);
                 } catch(Exception whoops) {
                     e = whoops;
                 }

                 System.assertEquals(null, e);
                 System.assertEquals(
                     productionClass, 
                     anotherClassUsingProductionClass.productionClass
                 );
            }

            // etc...  

        }
        Test.stopTest();

    }

}

Mocking Framework

Below uses a helper class: Debuggable. See https://github.com/flexchecks/debugger


public with sharing Class SoapApi {

    public with sharing InvalidMockRequestException extends Exception {}
    public with sharing NullRequestException extends Exception {}
    public with sharing NullRequestTypeException extends Exception {}
    public with sharing NullRequestTypeException extends Exception {}
    public with sharing InvalidMockResponseException extends Exception {}

    /*
    █████╗ ██████╗ █████╗  ██╗  ██╗██████╗██████╗██████╗██████╗
    ██╔═██╗██╔═══╝██╔═══██╗██║  ██║██╔═══╝██╔═══╝╚═██╔═╝██╔═══╝
    █████╔╝████╗  ██║   ██║██║  ██║████╗  ██████╗  ██║  ██████╗
    ██╔═██╗██╔═╝  ██║▄▄ ██║██║  ██║██╔═╝  ╚═══██║  ██║  ╚═══██║
    ██║ ██║██████╗╚██████╔╝╚█████╔╝██████╗██████║  ██║  ██████║
    ╚═╝ ╚═╝╚═════╝ ╚══▀▀═╝  ╚════╝ ╚═════╝╚═════╝  ╚═╝  ╚═════╝
    */
    /**
     * Base interface for Requests that allows full customization
     */
    public interface Requestable extends Debuggable {
        Type getType();
        Type getResponseType();
        Type getMockResponseType();
        String getSoapAction();
        String getRequestNamespace();
        String getRequestName();
        String getResponseNamespace();
        String getResponseName();
    }

    /**
     * Most Request implementations extend this class
     */
    public abstract class Request implements SoapApi.Requestable {
        public abstract Type getType();
        public abstract Type getResponseType();

        public virtual String getRequestName() {
            final Type requestType = this.getType();
            return requestType == null ? null : requestType.getName();
        }

        public virtual String getResponseName() {
            final Type responseType = this.getResponseType();
            return requestType == null ? null : requestType.getName();
        }
    }

    /*
    ██████╗ ███████╗███████╗██████╗  ██████╗ ███╗   ██╗███████╗███████╗███████╗
    ██╔══██╗██╔════╝██╔════╝██╔══██╗██╔═══██╗████╗  ██║██╔════╝██╔════╝██╔════╝
    ██████╔╝█████╗  ███████╗██████╔╝██║   ██║██╔██╗ ██║███████╗█████╗  ███████╗
    ██╔══██╗██╔══╝  ╚════██║██╔═══╝ ██║   ██║██║╚██╗██║╚════██║██╔══╝  ╚════██║
    ██║  ██║███████╗███████║██║     ╚██████╔╝██║ ╚████║███████║███████╗███████║
    ╚═╝  ╚═╝╚══════╝╚══════╝╚═╝      ╚═════╝ ╚═╝  ╚═══╝╚══════╝╚══════╝╚══════╝
    */
    public interface Responsive extends Debuggable {
        void doBeforeReturn();
    }

    public abstract class Response implements SoapApi.Responsive {
        public virtual void doBeforeReturn() {
            // Default: do nothing
        }
    }

    /*
    ███╗   ███╗ █████╗  █████╗██╗  ██╗
    ████╗ ████║██╔══██╗██╔═══╝██║ ██╔╝
    ██╔████╔██║██║  ██║██║    █████╔╝ 
    ██║╚██╔╝██║██║  ██║██║    ██╔═██╗ 
    ██║ ╚═╝ ██║╚█████╔╝╚█████╗██║  ██╗
    ╚═╝     ╚═╝ ╚════╝  ╚════╝╚═╝  ╚═╝
    */
    /*
    █████╗ ██████╗██████╗██████╗  █████╗ ███╗   ██╗██████╗██████╗
    ██╔═██╗██╔═══╝██╔═══╝██╔══██╗██╔══██╗████╗  ██║██╔═══╝██╔═══╝
    █████╔╝████╗  ██████╗██████╔╝██║  ██║██╔██╗ ██║██████╗████╗  
    ██╔═██╗██╔═╝  ╚═══██║██╔═══╝ ██║  ██║██║╚██╗██║╚═══██║██╔═╝  
    ██║ ██║██████╗██████║██║     ╚█████╔╝██║ ╚████║██████║██████╗
    ╚═╝ ╚═╝╚═════╝╚═════╝╚═╝      ╚════╝ ╚═╝  ╚═══╝╚═════╝╚═════╝
    */
    public abstract class MockResponse 
        extends Response 
        implements WebServiceMock 
    {
        public abstract Object getResponse(Requestable request);

        protected Object response;

        // WebServiceMock
        public void doInvoke(
            Object stub,
            Object request,
            Map<String, Object> response,
            String endpoint,
            String soapAction,
            String requestName,
            String responseNamespace,
            String responseName,
            String responseType
        ) {
            if(!(request instanceof Requestable)) {
                new InvalidMockRequestException(String.format(
                    System.Label.SoapApi_InvalidMockRequestException, 
                    new String[] {
                        // Message parameters
                    }
                ))
            }
            if(this.response == null) {
                this.response = this.getResponse((Requestable) request);
            }
            response.put('response_x', this.response);
        }  
    }

    /*
    ██╗███╗   ██╗███████╗████████╗ █████╗ ███╗   ██╗ ██████╗███████╗
    ██║████╗  ██║██╔════╝╚══██╔══╝██╔══██╗████╗  ██║██╔════╝██╔════╝
    ██║██╔██╗ ██║███████╗   ██║   ███████║██╔██╗ ██║██║     █████╗  
    ██║██║╚██╗██║╚════██║   ██║   ██╔══██║██║╚██╗██║██║     ██╔══╝  
    ██║██║ ╚████║███████║   ██║   ██║  ██║██║ ╚████║╚██████╗███████╗
    ╚═╝╚═╝  ╚═══╝╚══════╝   ╚═╝   ╚═╝  ╚═╝╚═╝  ╚═══╝ ╚═════╝╚══════╝
    */
    public abstract String getEndpoint();

    /**
     * Makes API call.  Sets Mock if Test.isRunningTest()
     * @param  request Requestable
     * @return         Responsive
     */
    public Responsive send(SoapApi.Requestable request) {
        if(request == null) {
            throw new NullRequestException(String.format(
                System.Label.SoapApi_NullRequestException, 
                new String[] {
                    // Message parameters
                }
            ));
        }

        final Type requestType = request.getType();
        if(requestType == null) {
            throw new NullRequestTypeException(String.format(
                System.Label.SoapApi_NullRequestTypeException, 
                new String[] {
                    // Message parameters
                }
            ));
        }

        final Type responseType = request.getResponseType();
        if(responseType == null) {
            throw new NullRequestTypeException(String.format(
                System.Label.SoapApi_NullResponseTypeException, 
                new String[] {
                    // Message parameters
                }
            ));
        }

        // Debug SOAP call.  See https://github.com/flexchecks/debugger
        Debugger debugger = new Debugger(request.getType().getName())
            .add('request', request);

        // Set Mock if running Test
        if(Test.isRunningTest()) {
            final Type mockResponseType = request.getMockResponseType();
            Object response = mockResponseType == null ? null : mockResponseType.newInstance();
            if(!(response instanceof MockResponse)) {
                throw new InvalidMockResponseException(String.format(
                    System.Label.SoapApi_InvalidMockResponseException, 
                    new String[] {
                        // Message parameters
                    }
                ));
            }
            Test.setMock(WebServiceMock.class, response);
        }

        // Invoke WebServiceCallout
        Map<String, Object> responses = new Map<String, Object> {
            'response_x' => null
        };

        String endpoint;
        try{
            endpoint = this.getEndpoint();
            debugger
                .add('Endpoint', endpoint)
                .debug();
            WebServiceCallout.invoke(
                this,
                request,
                responses,
                new String[]{
                    endpoint,
                    request.getSoapAction(),
                    request.getRequestNamespace(),
                    request.getRequestName(),
                    request.getResponseNamespace(),
                    request.getResponseName(),
                    responseType.getName()
                }
            );
        } catch(Exception e) {
            debugger
                .add(e)
                .debug();
            throw e;
        }

        // Verify Web Service sends Responsive
        if(!(responses.get('response_x') instanceof Responsive)) {
            throw new InvalidSendResponseException(String.format(
                System.Label.SoapApi_InvalidSendResponseException, 
                new String[] {
                    // Message parameters
                }
            ));
        }

        // Unpackage, debug, and return response
        Responsive response = (Responsive) responses.get('response_x');
        response.doBeforeReturn();
        debugger
            .add('response', response)
            .debug();

        return response;
    }

    /*
    ██╗  ██╗██████╗██╗██╗    
    ██║  ██║╚═██╔═╝██║██║    
    ██║  ██║  ██║  ██║██║    
    ██║  ██║  ██║  ██║██║    
    ╚█████╔╝  ██║  ██║██████╗
     ╚════╝   ╚═╝  ╚═╝╚═════╝
    */
   public static String[] getOptionalArrayTypeInfo(String field, String namespace) {
        return new String[]{
            field,
            namespace,
            null,
            '0', // minOccurs: '0' meaning optional
            '-1', // maxOccurs; '-1' = 'unbounded' => is an array
            'false' // nillible
        };
    }

    public static String[] getNillableOptionalArrayTypeInfo(String field, String namespace) {
        return new String[]{
            field,
            namespace,
            null,
            '0', // minOccurs: '0' meaning optional
            '-1', // maxOccurs; '-1' = 'unbounded' => is an array
            'true' // nillible
        };
    }

    public static String[] getOptionalFieldTypeInfo(String field, String namespace) {
        return new String[]{
            field,
            namespace,
            null,
            '0', // minOccurs: '0' meaning is optional
            '1', // maxOccurs; '1' meaning at most 1 occurance => is not an array.
            'false' // nillible
        };
    }

    public static String[] getNillableOptionalFieldTypeInfo(String field, String namespace) {
        return new String[]{
            field,
            namespace,
            null,
            '0', // minOccurs: '0' meaning is optional
            '1', // maxOccurs; '1' meaning at most 1 occurance => is not an array.
            'true' // nillible
        };
    }

    public static String[] getRequiredArrayTypeInfo(String field, String namespace) {
        return new String[]{
            field,
            namespace,
            null,
            '1', // minOccurs: '1' meaning 1 occurance is required
            '-1', // maxOccurs; '-1' = 'unbounded' => is an array
            'false' // nillible
        };
    }

    public static String[] getNillableRequiredArrayTypeInfo(String field, String namespace) {
        return new String[]{
            field,
            namespace,
            null,
            '1', // minOccurs: '1' meaning 1 occurance is required
            '-1', // maxOccurs; '-1' = 'unbounded' => is an array
            'true' // nillible
        };
    }

    public static String[] getRequiredFieldTypeInfo(String field, String namespace) {
        return new String[]{
            field,
            namespace,
            null,
            '1', // minOccurs: '1' meaning 1 occurance is required
            '1', // maxOccurs; '1' meaning at most 1 occurance => is not an array.
            'false' // nillible
        };
    }

    public static String[] getNillableRequiredFieldTypeInfo(String field, String namespace) {
        return new String[]{
            field,
            namespace,
            null,
            '1', // minOccurs: '1' meaning 1 occurance is required
            '1', // maxOccurs; '1' meaning at most 1 occurance => is not an array.
            'true' // nillible
        };
    }

    public static String[] getApexSchemaTypeInfo(
        String namespace,
        Boolean isElementFormDeafultQualified, 
        Boolean isAttributeFormDeafultQualified
    ) {
        return new String[]{
            namespace,
            isElementFormDeafultQualified == true ? 'true' : 'false',
            isAttributeFormDeafultQualified == true ? 'true' : 'false'
        };
    }
}

/*
██████╗██╗  ██╗ █████╗ ███╗   ███╗██████╗ ██╗    ██████╗
██╔═══╝╚██╗██╔╝██╔══██╗████╗ ████║██╔══██╗██║    ██╔═══╝
████╗   ╚███╔╝ ███████║██╔████╔██║██████╔╝██║    ████╗  
██╔═╝   ██╔██╗ ██╔══██║██║╚██╔╝██║██╔═══╝ ██║    ██╔═╝  
██████╗██╔╝ ██╗██║  ██║██║ ╚═╝ ██║██║     ██████╗██████╗
╚═════╝╚═╝  ╚═╝╚═╝  ╚═╝╚═╝     ╚═╝╚═╝     ╚═════╝╚═════╝
*/
@IsTest
public with sharing class ThirdParty {
    /**
     * Namespace
     */
    public static final String NS = 'https://api.third-party.biz/1.0';

    /*
    ██╗  ██╗██████╗██╗██╗    
    ██║  ██║╚═██╔═╝██║██║    
    ██║  ██║  ██║  ██║██║    
    ██║  ██║  ██║  ██║██║    
    ╚█████╔╝  ██║  ██║██████╗
     ╚════╝   ╚═╝  ╚═╝╚═════╝
    */
    public static String[] o(String field) {
        return SoapApi.getOptionalFieldTypeInfo(field, NS);
    }

    public static String[] n(String field) {
        return SoapApi.getNillableOptionalFieldTypeInfo(field, NS);
    }

    public static String[] a(String field) {
        return SoapApi.getNillableOptionalArrayTypeInfo(field, NS);
    }

    public static String[] s() {
        return SoapApi.getApexSchemaTypeInfo(NS, true, false);
    }

    /*
    █████╗ ██████╗ █████╗  ██╗  ██╗██████╗██████╗██████╗
    ██╔═██╗██╔═══╝██╔═══██╗██║  ██║██╔═══╝██╔═══╝╚═██╔═╝
    █████╔╝████╗  ██║   ██║██║  ██║████╗  ██████╗  ██║  
    ██╔═██╗██╔═╝  ██║▄▄ ██║██║  ██║██╔═╝  ╚═══██║  ██║  
    ██║ ██║██████╗╚██████╔╝╚█████╔╝██████╗██████║  ██║  
    ╚═╝ ╚═╝╚═════╝ ╚══▀▀═╝  ╚════╝ ╚═════╝╚═════╝  ╚═╝  
    */
    public abstract with sharing class Request extends SoapApi.Request {
        public String getSoapAction() {
            return String.join(
                new String[] {
                    NS, 
                    'ICompanyAPI', 
                    this.getRequestName()
                }, 
                '/'
            );
        }

        public String getRequestNamespace() {
            return NS;
        }

        public String getResponseNamespace() {
            return NS;
        }
    }

    /*
     █████╗ █████╗ █████╗  █████╗ ██╗   ██╗   █████╗ ██████╗
    ██╔══██╗██╔═██╗██╔═██╗██╔══██╗╚██╗ ██╔╝  ██╔══██╗██╔═══╝
    ███████║█████╔╝█████╔╝███████║ ╚████╔╝   ██║  ██║████╗  
    ██╔══██║██╔═██╗██╔═██╗██╔══██║  ╚██╔╝    ██║  ██║██╔═╝  
    ██║  ██║██║ ██║██║ ██║██║  ██║   ██║     ╚█████╔╝██║    
    ╚═╝  ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝  ╚═╝   ╚═╝      ╚════╝ ╚═╝    
    */
    /*
    ██████╗██████╗█████╗ ██╗███╗   ██╗ █████╗ 
    ██╔═══╝╚═██╔═╝██╔═██╗██║████╗  ██║██╔═══╝ 
    ██████╗  ██║  █████╔╝██║██╔██╗ ██║██║ ███╗
    ╚═══██║  ██║  ██╔═██╗██║██║╚██╗██║██║  ██║
    ██████║  ██║  ██║ ██║██║██║ ╚████║╚█████╔╝
    ╚═════╝  ╚═╝  ╚═╝ ╚═╝╚═╝╚═╝  ╚═══╝ ╚════╝ 
    */
    public with sharing class ArrayOfString {
        // @Version-3.0.0
        public String[] string_x;
        @TestVisible String[] string_x_type_info = n('string');

        // Apex Schema and Field Order
        @TestVisible String[] apex_schema_type_info = s();
        @TestVisible String[] field_order_type_info = new String[] {
            'string_x'
        };

        // Debuggable
        public Map<String, Object> debug() {
            return new Map<String, Object>{
                'Strings' => this.getStrings()
            };
        }
    }

    /*
     █████╗ ██████╗██████╗
    ██╔═══╝ ██╔═══╝╚═██╔═╝
    ██║ ███╗████╗    ██║  
    ██║  ██║██╔═╝    ██║  
    ╚█████╔╝██████╗  ██║  
     ╚════╝ ╚═════╝  ╚═╝  
    */
    /*
     █████╗ █████╗ ███╗   ███╗██████╗  █████╗ ███╗   ██╗██╗   ██╗
    ██╔═══╝██╔══██╗████╗ ████║██╔══██╗██╔══██╗████╗  ██║╚██╗ ██╔╝
    ██║    ██║  ██║██╔████╔██║██████╔╝███████║██╔██╗ ██║ ╚████╔╝ 
    ██║    ██║  ██║██║╚██╔╝██║██╔═══╝ ██╔══██║██║╚██╗██║  ╚██╔╝  
    ╚█████╗╚█████╔╝██║ ╚═╝ ██║██║     ██║  ██║██║ ╚████║   ██║   
     ╚════╝ ╚════╝ ╚═╝     ╚═╝╚═╝     ╚═╝  ╚═╝╚═╝  ╚═══╝   ╚═╝   
    */
    /*
     █████╗ █████╗ █████╗ ██████╗██████╗
    ██╔═══╝██╔══██╗██╔═██╗██╔═══╝██╔═══╝
    ██║    ██║  ██║██║ ██║████╗  ██████╗
    ██║    ██║  ██║██║ ██║██╔═╝  ╚═══██║
    ╚█████╗╚█████╔╝█████╔╝██████╗██████║
     ╚════╝ ╚════╝ ╚════╝ ╚═════╝╚═════╝
    */
    public with sharing class GetCompanyCodes extends Request {
        // Apex Schema and Field Order
        @TestVisible String[] apex_schema_type_info = s();
        @TestVisible String[] field_order_type_info = new String[] {}''

        // SoapApi.Request
        public override Type getType() {
            return GetCompanyCodes.class;
        }   

        public override Type getResponseType() {
            return GetCompanyCodesResponse.class;
        }

        public Type getMockResponseType() {
            return ThirdParty_Factory.GetCompanyCodesMockResponse.class;
        }

        // Debuggable
        public Map<String, Object> debug() {
            return new Map<String, Object>{};
        }
    }

    public virtual with sharing class GetCompanyCodesResponse extends SoapApi.Response {
        public ArrayOfString GetCompanyCodesResult;
        String[] GetCompanyCodesResult_type_info = n('GetCompanyCodesResult');
        String[] apex_schema_type_info = s();
        String[] field_order_type_info = new String[]{'GetCompanyCodesResult'};

        public String[] getCompanyCodes() {
            return this.GetCompanyCodesResult == null ? new String[] {} : this.GetCompanyCodesResult.string_x;
        }

        // Debuggable
        public Map<String, Object> debug() {
            return new Map<String, Object>{
                'GetCompanyCodesResult' => this.GetCompanyCodesResult
            };
        }
    }

    /*
     █████╗ █████╗ ███╗   ███╗██████╗  █████╗ ███╗   ██╗██╗   ██╗
    ██╔═══╝██╔══██╗████╗ ████║██╔══██╗██╔══██╗████╗  ██║╚██╗ ██╔╝
    ██║    ██║  ██║██╔████╔██║██████╔╝███████║██╔██╗ ██║ ╚████╔╝ 
    ██║    ██║  ██║██║╚██╔╝██║██╔═══╝ ██╔══██║██║╚██╗██║  ╚██╔╝  
    ╚█████╗╚█████╔╝██║ ╚═╝ ██║██║     ██║  ██║██║ ╚████║   ██║   
     ╚════╝ ╚════╝ ╚═╝     ╚═╝╚═╝     ╚═╝  ╚═╝╚═╝  ╚═══╝   ╚═╝   
    */
    public  with sharing class Company {
        public String CompanyCode, CompanyName, DBAName;
        public Boolean IsActive;
        public DateTime CreatedDate;
        public Integer Employees;

        @TestVisible String[] CompanyCode_type_info = o('CompanyCode');
        @TestVisible String[] CompanyName_type_info = o('CompanyName');
        @TestVisible String[] DBAName_type_info = n('DBAName');
        @TestVisible String[] IsActive_type_info = o('IsActive');
        @TestVisible String[] CreatedDate_type_info = n('CreatedDate');
        @TestVisible String[] Employees_type_info = n('Employees');

        // Apex Schema and Field Order
        @TestVisible String[] apex_schema_type_info = s();
        @TestVisible String[] field_order_type_info = new String[] {
            'CompanyCode', 
            'CompanyName', 
            'DBAName', 
            'IsActive', 
            'CreatedDate', 
            'Employees'
        };

        // Debuggable
        public Map<String, Object> debug() {
            return new Map<String, Object>{
                'CompanyCode/CompanyName' => String.join(
                    new Object[] {
                        this.ComapnyCode, 
                        this.CompanyName
                    }, 
                    '/'
                ), 
                'IsActive/Employees' => String.join(
                    new Object[] {
                        this.IsActive, 
                        this.Employees
                    }, 
                    '/'
                )
            };
        }
    }

    public with sharing class ArrayOfCompany {
        public Company[] Company;
        @TestVisible String[] Company_type_info = a('Company');

        // Apex Schema and Field Order
        @TestVisible String[] apex_schema_type_info = s();
        @TestVisible String[] field_order_type_info = new String[] {
            'Company'
        };

        // Debuggable
        public Map<String, Object> debug() {
            return new Map<String, Object> {
                'Company' => this.Company
            };
        }
    }

    /*
     █████╗ ██████╗██████╗
    ██╔═══╝ ██╔═══╝╚═██╔═╝
    ██║ ███╗████╗    ██║  
    ██║  ██║██╔═╝    ██║  
    ╚█████╔╝██████╗  ██║  
     ╚════╝ ╚═════╝  ╚═╝  
    */
    /*
     █████╗ █████╗ ███╗   ███╗██████╗  █████╗ ███╗   ██╗██╗██████╗██████╗
    ██╔═══╝██╔══██╗████╗ ████║██╔══██╗██╔══██╗████╗  ██║██║██╔═══╝██╔═══╝
    ██║    ██║  ██║██╔████╔██║██████╔╝███████║██╔██╗ ██║██║████╗  ██████╗
    ██║    ██║  ██║██║╚██╔╝██║██╔═══╝ ██╔══██║██║╚██╗██║██║██╔═╝  ╚═══██║
    ╚█████╗╚█████╔╝██║ ╚═╝ ██║██║     ██║  ██║██║ ╚████║██║██████╗██████║
     ╚════╝ ╚════╝ ╚═╝     ╚═╝╚═╝     ╚═╝  ╚═╝╚═╝  ╚═══╝╚═╝╚═════╝╚═════╝
    */
    public with sharing class GetCompanies extends Request {
        public ArrayOfString CompanyCodes;
        @TestVisible String[] CompanyCodes_type_info = n('CompanyCodes');

        // Apex Schema and Field Order
        @TestVisible String[] apex_schema_type_info = s();
        @TestVisible String[] field_order_type_info = new String[] {
            'CompanyCodes'
        };

        // SoapApi.Request
        public override Type getType() {
            return GetCompanies.class;
        }   

        public override Type getResponseType() {
            return GetCompaniesResponse.class;
        }

        public Type getMockResponseType() {
            return ThirdParty_Factory.GetCompaniesMockResponse.class;
        }
        // Debuggable
        public Map<String, Object> debug() {
            return new Map<String, Object> {};
        }
    }

    public with sharing class GetCompaniesResponse extends SoapApi.Response {
        public ArrayOfCompany GetCompaniesResult;
        @TestVisible String[] GetCompaniesResult_type_info = n('GetCompaniesResult');
        @TestVisible String[] apex_schema_type_info = s();
        @TestVisible String[] field_order_type_info = new String[] {
            'GetCompaniesResult'
        };

        // GetResponse
        public Company[] getCompanies() {
            return this.GetCompaniesResult == null || this.GetCompaniesResult.Company == null ? new Company[] {} : this.GetCompaniesResult.Company;
        }

        // Debuggable
        public Map<String, Object> debug() {
            return new Map<String, Object>{
                'GetCompaniesResult' => this.GetCompaniesResult
            };
        }
    }

    /*
    ██████╗ █████╗  █████╗ ██████╗  █████╗ ██████╗ ██╗
    ██╔═══╝██╔══██╗██╔══██╗██╔══██╗██╔══██╗██╔══██╗██║
    ██████╗██║  ██║███████║██████╔╝███████║██████╔╝██║
    ╚═══██║██║  ██║██╔══██║██╔═══╝ ██╔══██║██╔═══╝ ██║
    ██████║╚█████╔╝██║  ██║██║     ██║  ██║██║     ██║
    ╚═════╝ ╚════╝ ╚═╝  ╚═╝╚═╝     ╚═╝  ╚═╝╚═╝     ╚═╝
    */
    public with sharing class Api extends SoapApi {
        // Uncomment parameters if needed
    /*
        public Map<String,String> outputHttpHeaders_x;
        public String clientCertName_x;
        public String clientCert_x;
        public String clientCertPasswd_x;
    */
        public Integer timeout_x = 60000; // Milliseconds
        public String[] ns_map_type_info = new String[] {
            NS, 'ThirdParty'
        };
        public Map<String, String> inputHttpHeaders_x;

        public override String getEndpoint() {
            return 'callout:ThirdParty/CompanyAPI.svc';
        }

        public Api() {
            super();
        }

        /*
         ██████╗ ███████╗████████╗
        ██╔════╝ ██╔════╝╚══██╔══╝
        ██║  ███╗█████╗     ██║   
        ██║   ██║██╔══╝     ██║   
        ╚██████╔╝███████╗   ██║   
         ╚═════╝ ╚══════╝   ╚═╝   
        */
        public String[] getCompanyCodes() {
            return ((GetCompanyCodesResponse) this.send(new GetCompanyCodes())).getCompanyCodes();
        }

        public Company[] getCompanies() {
            return ((GetCompaniesResponse) this.send(new GetCompanies())).getCompanies();
        }

        public Company[] getCompanies(String[] companyCodes) {
            final GetCompanies request = new GetCompanies();
            {
                ArrayOfString arrayOfString = new ArrayOfString();
                {
                    arrayOfString.string_x = companyCodes;
                }
                request.companyCodes = arrayOfString;
            }
            return ((GetCompaniesResponse) this.send(request)).getCompanies();
        }
    }
}

@IsTest
public with sharing class ThirdParty_Factory {

    /*
     █████╗ ██████╗██████╗
    ██╔═══╝ ██╔═══╝╚═██╔═╝
    ██║ ███╗████╗    ██║  
    ██║  ██║██╔═╝    ██║  
    ╚█████╔╝██████╗  ██║  
     ╚════╝ ╚═════╝  ╚═╝  
    */
    /*
     █████╗ █████╗ ███╗   ███╗██████╗  █████╗ ███╗   ██╗██╗   ██╗  
    ██╔═══╝██╔══██╗████╗ ████║██╔══██╗██╔══██╗████╗  ██║╚██╗ ██╔╝  
    ██║    ██║  ██║██╔████╔██║██████╔╝███████║██╔██╗ ██║ ╚████╔╝   
    ██║    ██║  ██║██║╚██╔╝██║██╔═══╝ ██╔══██║██║╚██╗██║  ╚██╔╝    
    ╚█████╗╚█████╔╝██║ ╚═╝ ██║██║     ██║  ██║██║ ╚████║   ██║     
     ╚════╝ ╚════╝ ╚═╝     ╚═╝╚═╝     ╚═╝  ╚═╝╚═╝  ╚═══╝   ╚═╝     
    */
    /*
     █████╗ █████╗ █████╗ ██████╗
    ██╔═══╝██╔══██╗██╔═██╗██╔═══╝
    ██║    ██║  ██║██║ ██║████╗  
    ██║    ██║  ██║██║ ██║██╔═╝  
    ╚█████╗╚█████╔╝█████╔╝██████╗
     ╚════╝ ╚════╝ ╚════╝ ╚═════╝
    */
    public static final String[] COMPANY_CODES = new String[] {
        'DEMO', 
        'TEST', 
        '867-5309'
    };

    public with class GetCompanyCodesMockResponse extends SoapApi.MockResponse {
        public override Object getResponse(SoapApi.Requestable requestable) {
            ThirdParty.GetCompanyCodesResponse response = new ThirdParty.GetCompanyCodesResponse();
            {
                response.GetCompanyCodesResult = new ThirdParty.ArrayOfString();
                {
                    response.GetCompanyCodesResult.string_x = COMPANY_CODES.clone();
                }
            }
            return response;
        }
    }

    /*
     █████╗ ██████╗██████╗
    ██╔═══╝ ██╔═══╝╚═██╔═╝
    ██║ ███╗████╗    ██║  
    ██║  ██║██╔═╝    ██║  
    ╚█████╔╝██████╗  ██║  
     ╚════╝ ╚═════╝  ╚═╝  
    */
    /*
     █████╗ █████╗ ███╗   ███╗██████╗  █████╗ ███╗   ██╗██╗██████╗██████╗
    ██╔═══╝██╔══██╗████╗ ████║██╔══██╗██╔══██╗████╗  ██║██║██╔═══╝██╔═══╝
    ██║    ██║  ██║██╔████╔██║██████╔╝███████║██╔██╗ ██║██║████╗  ██████╗
    ██║    ██║  ██║██║╚██╔╝██║██╔═══╝ ██╔══██║██║╚██╗██║██║██╔═╝  ╚═══██║
    ╚█████╗╚█████╔╝██║ ╚═╝ ██║██║     ██║  ██║██║ ╚████║██║██████╗██████║
     ╚════╝ ╚════╝ ╚═╝     ╚═╝╚═╝     ╚═╝  ╚═╝╚═╝  ╚═══╝╚═╝╚═════╝╚═════╝
    */
    public static ThirdParty.Company getCompany(String companyCode) {
        final ThirdParty.Company company = new ThirdParty.Company();
        {
            company.CompanyCode = companyCode;
            company.CompanyName = companyCode;
            company.IsActive = true;
            company.Employees = Math.abs(Crypto.getRandomInteger());
        }
        return company;
    }

    public static ThirdParty.Company[] getCompanies(String[] companyCodes) {
        final ThirdParty.Company[] companies = new ThirdParty.Company[] {};
        for(String companyCode : (comapanyCodes == null ? COMPANY_CODES : companyCodes) {
            companies.add(getSysCompanyAvail(companyCode));
        }
        return companies;
    }

    public static ThirdParty.ArrayOfCompany getArrayOfCompany(ArrayOfString companyCodes) {
        final ThirdParty.ArrayOfCompany arrayOfCompany = new ThirdParty.ArrayOfCompany();
        {
            arrayOfCompany.Company = getCompanies(companyCodes == null ? null : companyCodes.string_x);
        }
        return arrayOfCompany;
    }

    public with sharing class GetCompaniesMockResponse extends SoapApi.MockResponse {
        public override Object getResponse(SoapApi.Requestable requestable) {
            ThirdParty.GetCompanies request = (ThirdParty.GetCompanies) requestable;
            ThirdParty.GetCompaniesResponse response = new ThirdParty.GetCompaniesResponse();
            {
                response.GetCompaniesResult = getArrayOfCompany(request.CompanyCodes);
            }
            return response;
        }
    }
}
christiancoleman commented 6 years ago

I like your insight into the FFLIB. The parts of our general readme don't address fflib in detail yet. I'm hoping someone much more skilled/versed in that regard can take it up if they have time before I do. Another thing: for both the explanation of how to use FFLIB and the caveats that you mention, I think it could be beneficial to break those apart into separate documents so that the main readme isn't too large to consume. We want developers to have a relatively manageable list of things to be aware of and if they need/want more information with the more complicated and nuanced recommendations we can refer them to something like this.

What do you think?

scottpelak commented 6 years ago

I agree. Maybe have a library of READMEs talking about different topics.

We could also develop a repo of Utility classes to implement the best practices (perhaps here? or in a public repo? Or a repo that's MIL-specific?).

christiancoleman commented 6 years ago

I would absolutely love to have a section dedicated to shared code. I'm not aware of the org structure in all of our Salesforce solutions or if we use only one org, but as or if we move to new orgs then having utility classes that can be used regardless of org can be a tremendous help. For example, something like a recordType utility helper.

I say go for it if you have extra time.

scottpelak commented 6 years ago

Maybe MIL Corp should create a Managed Package that contains common Utilities/best-practices. Then as they service more Customers/Orgs, they can install the Managed Package instead of copying-and-pasting the code.

Plus, Apex in Managed Packages doesn't count towards Apex Character Limit (though it doesn't seem like limits are generally a concern for government Orgs...)