0

I have seen other postings about creating a test class for invocable methods, but not one for a string variable. I am way green on this effort and can't get my head around building a test class to support this one. This apex class supports my Flow in using a string return variable and splits it to a collection array called Datevalue. The string value that comes back is:

["07/02/2024 1 PM - 5 PM","07/17/2024 9 AM - 1 PM","07/17/2024 1 PM - 5 PM","07/18/2024 9 AM - 1 PM","07/18/2024 1 PM - 5 PM"].

One would think it would recognize as an array, but not, so the reason for the Apex class call below.

public class CallFromFlowClass {
    
    @InvocableMethod(label='Return Text Collection' description='it will return list of string')
    public static List<List<String>> returnString(List<String> input){
        List<List<String>> finalResult= new List<List<String>>();
        finalResult.add((input[0].substring(1,input[0].length()-1)).split(','));
        return finalResult;
    }
    
    @AuraEnabled
    public String dateValue{get;set;}
}

Here is what I found to work thanks to Derek getting me started! @isTest public class CallFromFlowClassTest {

@isTest
static void testReturnString() {
    // Test data
    List<String> input = new List<String>{ '[Hello,World,Test]' };
    
    // Expected result
    List<List<String>> expectedResult = new List<List<String>>();
    expectedResult.add(new List<String>{ 'Hello', 'World', 'Test' });
    
    // Call the method
    List<List<String>> result = CallFromFlowClass.returnString(input);
    
    // Verify the result
    System.assertEquals(expectedResult, result);
}

@isTest
static void testEmptyInput() {
    // Test data
    List<String> input = new List<String>{ '[]' };
    
    // Expected result
    List<List<String>> expectedResult = new List<List<String>>();
    expectedResult.add(new List<String>{ '' });
    
    // Call the method
    List<List<String>> result = CallFromFlowClass.returnString(input);
    
    // Verify the result
    System.assertEquals(expectedResult, result);
}

@isTest
static void testSingleElement() {
    // Test data
    List<String> input = new List<String>{ '[SingleElement]' };
    
    // Expected result
    List<List<String>> expectedResult = new List<List<String>>();
    expectedResult.add(new List<String>{ 'SingleElement' });
    
    // Call the method
    List<List<String>> result = CallFromFlowClass.returnString(input);
    
    // Verify the result
    System.assertEquals(expectedResult, result);
}

@isTest
static void testDateValueProperty() {
    // Create an instance of the class
    CallFromFlowClass instance = new CallFromFlowClass();
    
    // Set the dateValue property
    instance.dateValue = '2024-08-14';
    
    // Verify the dateValue property
    System.assertEquals('2024-08-14', instance.dateValue);
}

}

6
  • Most unit tests follow the exact same pattern, commonly called "AAA". Arrange (set up any required test data, you don't need anything more than a string here), Act (execute the method in the class that you're trying to test), Assert (gather results, make assertions so that you don't end up with a test that says 2 + 2 = 5 is fine).
    – Derek F
    Commented Aug 12 at 21:23
  • 1
    If you've already tried making a test class and a few test methods for this, then you should edit your question to include that attempt. Even if it doesn't work, it helps show that you're not trying to get work done for free and serves as a base for writing answers that help you (and others) learn and improve. If you're running into an error message, also include that (and the stack trace) verbatim.
    – Derek F
    Commented Aug 12 at 21:26
  • 1
    NB: Your implementation is rather incomplete as it does not handle all possible input values. You have assumed you can process just the first input (albeit an array of string), returning just a single result value (albeit a single array). This will fail when flow bulkification is applied.
    – Phil W
    Commented Aug 12 at 21:34
  • Thanks Derek, The issue is I dont know where to begin. Literaly as far as I have gotten. @IsTest Private class CallFromFlowClassTest { private static testMethod void TestString() { Test.startTest(); Test.stopTest(); } }
    – Mike
    Commented Aug 13 at 12:06
  • Thank you Phil. I control the return value as it is calling my API so the list string is set by me on the response.
    – Mike
    Commented Aug 13 at 12:07

3 Answers 3

2

Intro

90% of tests end up looking pretty much the same. There are a few special cases out there, but you're not running into any of those here. Like I said in my first comment, it pretty much all comes down to "AAA" (which can be contorted into "SEA")

  • Arrange (Set up the test environment)
  • Act (Execute the method in the class that you're trying to test)
  • Assert (Assertions are important. Gather the results from running your method and check that it meets expectations)

Worthy of note

You should be able to use the other resources you've found as a base. For the most part, the distinction between an Invocable method and any other method doesn't matter. Neither does the number and type of parameters that a method takes. As long as you create the appropriate hierarchy of data inside of the test class and are able to call the method your test is targeting, the test is going to look very similar to what you've already seen.

Practical example/advice

Probably the simplest test you could write here would be one to stress what happens when the input list is empty.

// The "isTest" annotation is required
// Doesn't matter if the class itself is public or private (or global, but we
//   should generally avoid using global unless you know that it's required)
@isTest
private class CallFromFlowClassTest {
    // Not used here, but just as a note
    // Static variables are cleared out before each individual test method is run
    //static String willBeCleared;
    //   ...unless you assign a value when they're declared
    //static Datetime now = Datetime.now();

    // It's generally encouraged to use a TestSetup annotated method to handle things
    //   like creating SObject records for tests.
    // It makes it so that the records are only created once (and Salesforce manages
    //   rolling back any changes made to them in a test before the next test runs)
    @TestSetup
    static void makeData() {
        // Nothing for you to do here in this particular case
    }

    // The "testmethod" keyword has been deprecated for several years at this point
    // Use the "isTest" annotation instead
    // This can be on the same line as the rest of the method signature, but keeping it
    //   on its own line makes it dead simple to comment out a test (if it's not marked
    //   as a test method, it won't get run automatically)
    // I've recently taken to <method name>_<inputs>_<result> naming for tests
    // Just a preference thing. I think it makes it easier to find things when tests fail.
    @isTest
    static void returnString_givenEmptyList_doesNotThrowException() {
        // Step 1: Arrange/Setup
        List<String> testInput = new List<String>();

        // Also not a bad idea to declare a variable to hold the results
        List<List<String>> results;

        // Step 2: Act/Execute the method
        // This is what you'll typically surround in starttest/stoptest
        // Basically just making a method call like you would anywhere else
        // Static method = call it directly instead of making an instance of the class
        Test.startTest();

        // In this scenario, it's possible that we could run into an exception
        // We could just let it bubble up the stack, but catching it in our test allows
        //   us to handle things a bit more gracefully
        try {
            // Since this try is its own block/scope, we needed to declare the results
            //   variable outside of the try (to be able to use it later, when we make assertions)
            results = CallFromFlowClass.returnString(testInput);
        } catch (Exception e) {
            // Including a descriptive message is extraordinarily helpful for debugging
            // It's technically optional, but you should always do it.
            // Including the exception (or e.getMessage() at the very least) is also
            //   extremely valuable
            Assert.fail('Ran into an exception we weren\'t expecting when running returnString(): ' + e);
        }
        Test.stopTest();

        // Step 3: Assert
        // Coverage is what Salesforce wants (really, it's the only metric they can enforce).
        // Assertions are what developers _need_. Assertions are the only thing preventing
        //   us from deploying code that says 2 + 2 = 5.
        // If your code changes data (returns a value, modifies class "state", performs DML,
        //   ...etc), and that data is public (e.g. not a private class variable), then
        //   it is a target for assertions.
        Assert.isNotEqual(null, results, 'result list of lists was null');
        Assert.isTrue(results.isEmpty(), 'result should have been empty');
    }

    // In general, you should have more than one test method in a test class
    // Keep the individual tests as small as practical, ideally only testing a single
    //   thing at a time.
    // That makes it easier to get a more complete picture of what's happening when test(s)
    //   fail. Certainly more so than having a single, big test method that fails part-way
    //   through (so you fix it, and something else breaks, so you fix it, and the first
    //   thing breaks again).

    // The more scenarios you test, the better your code coverage will be.
    // If you test enough scenarios, you won't need to worry about code coverage at all
    //   (because your coverage will naturally be very high)
    @isTest
    static void returnString_givenNull_doesNotThrowException() {
        // You could just declare a variable, and it'll be null by default if you don't
        //   assign anything to it
        // ...but being explicit is generally preferable to relying on implicit behavior
        List<String> testInput = null;
        List<List<String>> results;

        // startTest() and stopTest() give you a fresh set of limits for the
        //   code contained between them.
        // It's also required if you're testing async stuff (@future, queueable,
        //   batchable, schedulable), otherwise the async stuff won't be run
        //   as part of the test.
        Test.startTest();
        try {
            results = CallFromFlowClass.returnString(testInput);
        } catch (Exception e) {
            Assert.fail('Ran into an exception we weren\'t expecting when running returnString(): ' + e);
        }
        Test.stopTest();

        // The "Assert" class is newer (test.assertNotEqual() is the older style)
        // Either can be used, but the Assert class has more appropriate methods
        Assert.isNotEqual(null, results, 'result list of lists was null');
        Assert.isTrue(results.isEmpty(), 'result should have been empty');
    }
}

And without the wall of comments

@isTest
private class CallFromFlowClassTest {
    @TestSetup
    static void makeData() {
    }

    @isTest
    static void returnString_givenEmptyList_doesNotThrowException() {
        List<String> testInput = new List<String>();
        List<List<String>> results;

        Test.startTest();
        try {
            results = CallFromFlowClass.returnString(testInput);
        } catch (Exception e) {
            Assert.fail('Ran into an exception we weren\'t expecting when running returnString(): ' + e);
        }
        Test.stopTest();

        Assert.isNotEqual(null, results, 'result list of lists was null');
        Assert.isTrue(results.isEmpty(), 'result should have been empty');
    }

    @isTest
    static void returnString_givenNull_doesNotThrowException() {
        List<String> testInput = null;
        List<List<String>> results;

        Test.startTest();
        try {
            results = CallFromFlowClass.returnString(testInput);
        } catch (Exception e) {
            Assert.fail('Ran into an exception we weren\'t expecting when running returnString(): ' + e);
        }
        Test.stopTest();

        Assert.isNotEqual(null, results, 'result list of lists was null');
        Assert.isTrue(results.isEmpty(), 'result should have been empty');
    }
}

It's generally a good idea to write tests for both "positive" cases (where the input is well-formed/what you expect) as well as for "negative" cases (where the input is malformed/unexpected). Doing that helps to ensure that your code is robust. The two simple tests I've written as an example here would fall into the "negative case" camp.

From there, I hope the path forward is clear and that you'll be able to write the other tests. The rest of them ought to look pretty similar, you just need to change the input and adjust the assertions (you wouldn't want an empty result list if your input list actually has data in it).

Some other general advice:

  • Make sure that you create ALL of the data that your code needs to be able to run (except for some "Setup Objects" like User, Profile, RecordType, etc...)
  • Don't hard-code Ids into tests because that Id will not exist in the org you're deploying to (except for "Setup Objects", but even then hard-coding Ids makes things more difficult. Ask me how I know.)
  • If a test is failing (or was working but is now failing), resist the urge to change the tests. The problem is more likely with the code under test (or the test setup).
1
  • Derek thank you! This gave me a the frame work I needed! I found this to work!
    – Mike
    Commented Aug 14 at 12:28
0

If your invocable action is meant to accept a text string and output a list of strings (1:n for each record your flow handles), then you are on the right path on the implementation.

The test is rather simple, if you think about it: "this needs to test what happens when the action receives data, and if it outputs the appropriate result". In that regard, you can and should have more than one test method: one or more for the "happy paths" (cases where you expect input to be flawlessly accurate and valid) and the exceptions.

Within your test methods, you should be calling your invocable action with the parameters defined based on what the method is testing. For example:

@IsTest
private class MyInvocableClassTest {
    @IsTest
    static void test() {
        Test.startTest();
        List<List<String>> result = MyInvocableClass.execute(new List<String>{ 'random string' });
        Test.stopTest();

        // assertions go after the test, in general
        // for example: assert that "the list contains at least one element":
        Assert.isFalse(result.isEmpty(), 'Should have returned at least one result.');
        Assert.isTrue(result[0].size() > 1, 'Should have returned more than one string in the result, based on input.');
    }
}

What would happen here with a 'random string' as input? Your code would fail, right? Then test for that. Would it throw an exception? Then handle that too in the test, possibly in a different test method.

0

Thank you Derek and Renato, This helped me understand the process. This worked!

90% of tests end up looking pretty much the same. There are a few special cases out there, but you're not running into any of those here. Like I said in my first comment, it pretty much all comes down to "AAA" (which can be contorted into "SEA")

Arrange (Set up the test environment) Act (Execute the method in the class that you're trying to test) Assert (Assertions are important. Gather the results from running your method and check that it meets expectations) Worthy of note You should be able to use the other resources you've found as a base. For the most part, the distinction between an Invocable method and any other method doesn't matter. Neither does the number and type of parameters that a method takes. As long as you create the appropriate hierarchy of data inside of the test class and are able to call the method your test is targeting, the test is going to look very similar to what you've already seen.

Practical example/advice Probably the simplest test you could write here would be one to stress what happens when the input list is empty.

// The "isTest" annotation is required // Doesn't matter if the class itself is public or private (or global, but we // should generally avoid using global unless you know that it's required) @isTest private class CallFromFlowClassTest { // Not used here, but just as a note // Static variables are cleared out before each individual test method is run //static String willBeCleared; // ...unless you assign a value when they're declared //static Datetime now = Datetime.now();

// It's generally encouraged to use a TestSetup annotated method to handle things
//   like creating SObject records for tests.
// It makes it so that the records are only created once (and Salesforce manages
//   rolling back any changes made to them in a test before the next test runs)
@TestSetup
static void makeData() {
    // Nothing for you to do here in this particular case
}

// The "testmethod" keyword has been deprecated for several years at this point
// Use the "isTest" annotation instead
// This can be on the same line as the rest of the method signature, but keeping it
//   on its own line makes it dead simple to comment out a test (if it's not marked
//   as a test method, it won't get run automatically)
// I've recently taken to <method name>_<inputs>_<result> naming for tests
// Just a preference thing. I think it makes it easier to find things when tests fail.
@isTest
static void returnString_givenEmptyList_doesNotThrowException() {
    // Step 1: Arrange/Setup
    List<String> testInput = new List<String>();

    // Also not a bad idea to declare a variable to hold the results
    List<List<String>> results;

    // Step 2: Act/Execute the method
    // This is what you'll typically surround in starttest/stoptest
    // Basically just making a method call like you would anywhere else
    // Static method = call it directly instead of making an instance of the class
    Test.startTest();

    // In this scenario, it's possible that we could run into an exception
    // We could just let it bubble up the stack, but catching it in our test allows
    //   us to handle things a bit more gracefully
    try {
        // Since this try is its own block/scope, we needed to declare the results
        //   variable outside of the try (to be able to use it later, when we make assertions)
        results = CallFromFlowClass.returnString(testInput);
    } catch (Exception e) {
        // Including a descriptive message is extraordinarily helpful for debugging
        // It's technically optional, but you should always do it.
        // Including the exception (or e.getMessage() at the very least) is also
        //   extremely valuable
        Assert.fail('Ran into an exception we weren\'t expecting when running returnString(): ' + e);
    }
    Test.stopTest();

    // Step 3: Assert
    // Coverage is what Salesforce wants (really, it's the only metric they can enforce).
    // Assertions are what developers _need_. Assertions are the only thing preventing
    //   us from deploying code that says 2 + 2 = 5.
    // If your code changes data (returns a value, modifies class "state", performs DML,
    //   ...etc), and that data is public (e.g. not a private class variable), then
    //   it is a target for assertions.
    Assert.isNotEqual(null, results, 'result list of lists was null');
    Assert.isTrue(results.isEmpty(), 'result should have been empty');
}

// In general, you should have more than one test method in a test class
// Keep the individual tests as small as practical, ideally only testing a single
//   thing at a time.
// That makes it easier to get a more complete picture of what's happening when test(s)
//   fail. Certainly more so than having a single, big test method that fails part-way
//   through (so you fix it, and something else breaks, so you fix it, and the first
//   thing breaks again).

// The more scenarios you test, the better your code coverage will be.
// If you test enough scenarios, you won't need to worry about code coverage at all
//   (because your coverage will naturally be very high)
@isTest
static void returnString_givenNull_doesNotThrowException() {
    // You could just declare a variable, and it'll be null by default if you don't
    //   assign anything to it
    // ...but being explicit is generally preferable to relying on implicit behavior
    List<String> testInput = null;
    List<List<String>> results;

    // startTest() and stopTest() give you a fresh set of limits for the
    //   code contained between them.
    // It's also required if you're testing async stuff (@future, queueable,
    //   batchable, schedulable), otherwise the async stuff won't be run
    //   as part of the test.
    Test.startTest();
    try {
        results = CallFromFlowClass.returnString(testInput);
    } catch (Exception e) {
        Assert.fail('Ran into an exception we weren\'t expecting when running returnString(): ' + e);
    }
    Test.stopTest();

    // The "Assert" class is newer (test.assertNotEqual() is the older style)
    // Either can be used, but the Assert class has more appropriate methods
    Assert.isNotEqual(null, results, 'result list of lists was null');
    Assert.isTrue(results.isEmpty(), 'result should have been empty');
}

} And without the wall of comments

@isTest private class CallFromFlowClassTest { @TestSetup static void makeData() { }

@isTest
static void returnString_givenEmptyList_doesNotThrowException() {
    List<String> testInput = new List<String>();
    List<List<String>> results;

    Test.startTest();
    try {
        results = CallFromFlowClass.returnString(testInput);
    } catch (Exception e) {
        Assert.fail('Ran into an exception we weren\'t expecting when running returnString(): ' + e);
    }
    Test.stopTest();

    Assert.isNotEqual(null, results, 'result list of lists was null');
    Assert.isTrue(results.isEmpty(), 'result should have been empty');
}

@isTest
static void returnString_givenNull_doesNotThrowException() {
    List<String> testInput = null;
    List<List<String>> results;

    Test.startTest();
    try {
        results = CallFromFlowClass.returnString(testInput);
    } catch (Exception e) {
        Assert.fail('Ran into an exception we weren\'t expecting when running returnString(): ' + e);
    }
    Test.stopTest();

    Assert.isNotEqual(null, results, 'result list of lists was null');
    Assert.isTrue(results.isEmpty(), 'result should have been empty');
}

} It's generally a good idea to write tests for both "positive" cases (where the input is well-formed/what you expect) as well as for "negative" cases (where the input is malformed/unexpected). Doing that helps to ensure that your code is robust. The two simple tests I've written as an example here would fall into the "negative case" camp.

From there, I hope the path forward is clear and that you'll be able to write the other tests. The rest of them ought to look pretty similar, you just need to change the input and adjust the assertions (you wouldn't want an empty result list if your input list actually has data in it).

Some other general advice:

Make sure that you create ALL of the data that your code needs to be able to run (except for some "Setup Objects" like User, Profile, RecordType, etc...) Don't hard-code Ids into tests because that Id will not exist in the org you're deploying to (except for "Setup Objects", but even then hard-coding Ids makes things more difficult. Ask me how I know.) If a test is failing (or was working but is now failing), resist the urge to change the tests. The problem is more likely with the code under test (or the test setup).

4
  • You are not supposed to post your code as an answer. If you want, edit your original post, and if me and Derek helped you, choose one of the answer as the solution for your question. Commented Aug 14 at 12:37
  • seems I dont know how to use this either! Let me correct that
    – Mike
    Commented Aug 15 at 13:04
  • 1
    @Mike You should take a minute or two to go through the site tour.
    – Derek F
    Commented Aug 15 at 15:14
  • @DerekF Thank you for that. I certainly will!
    – Mike
    Commented Aug 16 at 16:05

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .