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).
2 + 2 = 5
is fine).