“Mere test-driven development” – A simple example of how and why

When I was a young developer, just starting out, I connected with a team I considered to be very advanced. On the team was an architect who seemed to orchestrate what the developers were making into more than the sum of the parts. When a developer finished their portion of a solution, the code didn’t just meet its requirement(s); the code became a building block to mix and match with other developers’ output to combine into solutions of a greater magnitude.

When I asked a developer on the team how exactly the architect was instructing them to produce results that was so accessible to link with the others on the team, they said, “he makes us write our tests first”.

Test-driven development (or TDD) is a way to make software using tests to clarify one’s thinking.

The idea is, before you start banging out code that is supposed to eventually do something, write a test to let you know if the code was successful or not. Then, ask “how well” and “under what conditions does the test still fail” and improve as much as possible. When you’re done, you’ll have code that does one thing really well!

That’s how the architect was able to work their magic: they had blocks that stacked without collapsing.

Right Thinking

As a teacher, I was introduced to this “Hierarchy of Questions”…

As one moves up the pyramid, higher-order thinking skills are employed. At the lowest level, one is really only accessing information as a trained response. For example, my dog remembers that ringing a bell makes the door open. He does not understand how, and he cannot advance to applying the bell solution to doors without bells.
A quick tangent: this makes interviews with questions like “what’s the syntax for…” undesirable since reference material is easily accessible in the real world. A developer who is really capable of producing enterprise level solutions needs to be able to approach their code from a higher level of thinking.
High-order thinking is the core value of TDD: it forces the developer to operate at the level of “Evaluation”. To evaluate something, we have to understand what the true goal is, imaging a solution we can apply, analyzing that solution, and then interpreting the results. We “begin with the end in mind”.

Activating lower levels of thinking is an academically sound way to move to higher levels. Once “evaluating” is accessed, the highest level (“creating”) can begin with more efficacy and purpose. TDD transforms the developer’s work product from “output” to “solution”. Some developers find TDD more stimulating because they always have a target.

Play nice with others

We are investing heavily in technologies and concepts in the world of SRE, Inner-Sourcing, CI/CD, micro-services and other DevOps sort of stuff. But unless our base-level code is developed thoughtfully, a number of downstream opportunities for efficiency will be unattainable. TDD code is more ready to be used in more efficient downstream processes than code that just “met the requirement”.

This happens as a natural result of adopting TDD: smaller, more resilient, code units. When testing, my discrete function does just one thing. That way I can easily output a PASS/FAIL value from testing said function. By contrast, if the function in that code unit I am testing did five things, predicting the readiness of the code to perform in all possible circumstances means not just testing whether my function actually did all 5 things it’s supposed to. To truly know the readiness of the function, I’d have to also output test results capturing all possible states of all five things at once and also in every sequence. Whew! Maybe an integration test will one day cover some of those combinations. But, when it comes to testing, each thing I ask a function to do exponentially increases the complexity of the test necessary to know if the function works.

When an architect (or should I say “code superintendent”) accepts functions with multiple, testable operations, use of that unit of code in combination with another unit may be impossibly complex. The architect is limiting their value by reducing the array of solutions that they could solve if they were allowed to mix and match micro-services. An ancillary benefit, having the smallest possible operation performed in each function means systems moving data between those functions (like a Lean 6 Sigma process) can also be tested more easily.

It’s counter-intuitive, but more enterprise value can be generated from a single, discrete, resilient code unit (tiny as that unit may be) each doing one thing than a monolith of code forming a function that solves all the requirements for an entire project:

Some benefits TDD can bring to an organization
  • Being able to automate unit tests and get an accurate assessment of the readiness of the code to perform its job is foundational to SRE
  • Knowing what the shared goal of what we’re trying to make is foundational to inner-sourcing.
  • Code discrete enough to plug-and-playing functions with each other and create loosely-coupled networks of operations is key to being able to utilize API gateway technology.
  • Edge computing becomes more possible when code can live far apart from each other and pass parameters to function when necessary (we can push as much as possible to the browser without it all having to go to the browser at once)
  • The benefits go on…

Some developers, like myself, find TDD rewarding because code comes out great at what it does (as opposed to OK at 10 things it sort of does).

There’s about 7 trillion writings out there on TDD, and about as many flavors, but the core concept is highly accessible. I titled this article “Mere test-driven development” because I want to give as vanilla an example as I can of the basics that everyone agrees make up the vital parts of TDD. Even the steps I use below are phrased arbitrarily so as to not stir up controversy about nuances of advanced TDD concepts. The goal is to go through in broad strokes and see if we can derive value from the basic process. Other information already exists on how to improve & refine the process.

Getting started with TDD is not hard. All you need to get started is an idea of something you’d like code to do.

For this example, I want my code to send an enqueued API call to an external system. Next, I’ll step that idea through the TDD process.

A TDD process example

Add code shell and test

I’ll script a small class, add parameters and a constructor, and an empty function name (where the action happens later). I’ve also added a test.

Some other elements of great code including comments, naming conventions, or headers have been trimmed to bring the TDD concepts center stage.

Don’t get hung up on every line of what the code does. If you understand the purpose of the comment blocks, that’s enough. Please don’t try to read the code to learn TDD. It’s not in there. The point is the code compiles and its needs tested.

The code includes five classes:

  • Request.cls – Inner class representing a HTTP request
  • Callout.cls – Makes a JSON REST based call out to an external service
  • CalloutJob.cls – Places job in queue for our system job runner to execute Callout.cls asyncronously
  • EnqueueCallout.cls – Provides access method to run CalloutJob.cls
  • CalloutJobTest.cls – Test the performance readiness of CalloutJob.cls

Take a quick glance at the code, then read notes about it down below…

Functional Code

Request.cls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
global class Request{
    //no arg constructor is required for flow interpretation
    global Request(){}
    //body
    @InvocableVariable(required=false)
    @AuraEnabled
    global String body;
    //method
    @InvocableVariable(required=true)
    @AuraEnabled
    global String method;
    //endpoint
    @InvocableVariable(required=true)
    @AuraEnabled
    global String namedCredential;
    //endpoint
    @InvocableVariable(required=true)
    @AuraEnabled
    global String endpoint;
    //passkey name
    @InvocableVariable(required=false)
    @AuraEnabled
    global String passkeyName;
    //constructor method
    global Request( String body,
                    String method,
                    String namedCredential,
                    String endpoint,
                    String passkeyName){
        this.body = body;
        this.method = method;
        this.namedCredential = namedCredential;
        this.endpoint = endpoint;
        this.passkeyName = passkeyName;
    }
}
CalloutJob.cls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CalloutJob implements Queueable {
    //this request will be available later on when the job runs
    public Request req;
    public CalloutJob(Request request){
        //we will keep this inner class representing the callout on deck until this job runs
        this.req = request;  
    }
    //execute callout when job runs later
    public void execute(QueueableContext context) {
        //Callout.toService(...)
    }
}
EnqueueCallout.cls
1
2
3
4
5
6
7
global class EnqueueCallout {
    //ask extenal service for information and return it
    @InvocableMethod(label='Enqueue Callout' description='Make the callout after current context ends')
    global static ID[]  createJob(Request[] requests) {
        return new Id[]{System.enqueueJob(new CalloutJob(requests[0]))};
    }
}
Callout.cls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
global class Callout{
    //ask extenal service for information and return it
    @InvocableMethod(label='toService' description='Will make a callout to service and return its response')
    global static Response[] toService(Request[] request) {
        //form the request
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:'+request[0].namedCredential+request[0].endpoint);
        req.setMethod(request[0].method);
        req.setHeader('Content-Type','application/json; charset=utf-8');
        if(request[0].passkeyName!=null)req.setHeader(request[0].passkeyName, '{!$Credential.Password}');
        if(!String.isBlank(request[0].body)) req.setBody(request[0].body);
        req.setTimeout(30000);
        //store the response
        HTTPResponse res = new Http().send(req);
        ////parse result to apex defined class
        return new Response[]{  new Response(   res.getBody(),
                                                res.getHeaderKeys(),
                                                headers(res),
                                                res.getStatus(),
                                                res.getStatusCode())};
    }
    //access the headers from the response and place them in an array
    private static String[] headers(HTTPResponse res){
        String[] headers = new String[]{};
        for(String key : res.getHeaderKeys()) headers.add(res.getHeader(key));
        return headers;
    }
}

For more information on this particular type of CRM code, please reference “Control Processes with Queueable Apex

Test Code

CalloutJobTest.cls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
@isTest
public class CalloutJobTest {
  //return the Request configuration values for this test
  static Test_Request_Configuration__mdt returnTestConfiguration(
    String configurationLabel
  ) {
    return [
      SELECT
        Body__c,
        Endpoint__c,
        Method__c,
        Named_Credential__c,
        Pass_Key_Name__c
      FROM Test_Request_Configuration__mdt
      WHERE MasterLabel = :configurationLabel
      LIMIT 1
    ];
  }
  //create instance of Request inner class when given a test request configuration
  static Request returnRequest(
    Test_Request_Configuration__mdt requestConfiguration
  ) {
    return new Request(
      requestConfiguration.Body__c,
      requestConfiguration.Method__c,
      requestConfiguration.Named_Credential__c,
      requestConfiguration.Endpoint__c,
      requestConfiguration.Pass_Key_Name__c
    );
  }
  public static AsyncApexJob jobInfo;
  static AsyncApexJob jobInfo(Id JobID) {
    return [
      SELECT MethodName, Status, NumberOfErrors, TotalJobItems
      FROM AsyncApexJob
      WHERE Id = :JobID
      LIMIT 1
    ];
  }
  //keep it DRY
  static void testResultBlock(Id JobID) {
    //access the state of the job
    AsyncApexJob jobInfo = JobID == null new AsyncApexJob() : jobInfo(JobID);
    //assert the job is done
    System.assertEquals('Completed', jobInfo.Status);
    //assert the request to call out did not produce errors
    System.assertEquals(0, jobInfo.NumberOfErrors);
  }
  static testMethod void testSinglePositiveCalloutJob() {
    //// given
    Request request = returnRequest(
      returnTestConfiguration('testSinglePositiveCalloutJob')
    );
    //// if...
    // startTest / stopTest block to force async processes to run
    Test.startTest();
    Test.setMock(HttpCalloutMock.classnew CalloutJobTestMockCall());
    ID JobID = EnqueueCallout.createJob(new List<Request>{ request })[0];
    Test.stopTest();
    //// then...
    testResultBlock(JobID);
  }
  static testMethod void testSingleNegativeCalloutJob() {
    //// given
    Test_Request_Configuration__mdt requestConfiguration = returnTestConfiguration(
      'testSingleNegativeCalloutJob'
    );
    //// if...
    // startTest / stopTest block to force async processes to run
    Test.startTest();
    Test.setMock(HttpCalloutMock.classnew CalloutJobTestMockCall());
    ID JobID = EnqueueCallout.createJob(new List<Request>{ request })[0];
    Test.stopTest();
    //// then...
    testResultBlock(JobID);
  }
  static testMethod void testNullCalloutJob() {
    //// given
    Test_Request_Configuration__mdt requestConfiguration = returnTestConfiguration(
      'testNullCalloutJob'
    );
    //// if...
    // startTest / stopTest block to force async processes to run
    Test.startTest();
    Test.setMock(HttpCalloutMock.classnew CalloutJobTestMockCall());
    ID JobID = EnqueueCallout.createJob(new List<Request>{ request })[0];
    Test.stopTest();
    //// then...
    testResultBlock(JobID);
  }
  static testMethod void testBulkPositiveCalloutJob() {
    //// given
    Test_Request_Configuration__mdt requestConfiguration = returnTestConfiguration(
      'testBulkPositiveCalloutJob'
    );
    //// if...
    // startTest / stopTest block to force async processes to run
    Test.startTest();
    Test.setMock(HttpCalloutMock.classnew CalloutJobTestMockCall());
    ID JobID = EnqueueCallout.createJob(new List<Request>{ request })[0];
    Test.stopTest();
    //// then...
    testResultBlock(JobID);
  }
  static testMethod void testBulkNegativeCalloutJob() {
    //// given
    Test_Request_Configuration__mdt requestConfiguration = returnTestConfiguration(
      'testBulkNegativeCalloutJob'
    );
    //// if...
    // startTest / stopTest block to force async processes to run
    Test.startTest();
    Test.setMock(HttpCalloutMock.classnew CalloutJobTestMockCall());
    ID JobID = EnqueueCallout.createJob(new List<Request>{ request })[0];
    Test.stopTest();
    //// then...
    testResultBlock(JobID);
  }
}

Few quick things about this code…

  • Apex makes you do code stuff to test call-outs that is not relevant to this article, so I’m not posting that code.
  • The purpose of CalloutJobTest.cls (as stated above) is to “Test the performance readiness of CalloutJob.cls". Though CalloutJob.cls references them implicitly or explicitly, Request.cls & EnqueueCallout.cls & Callout.cls (some whom themselves reference other classes) need to have their own tests for their own methods.
  • In terms of decomposition, this example is in some ways less than ideal. This is on purpose as a developer often has no control over some elements of the code they need to interact with. I want to provide a real world example of how to work a TDD process, even under less than ideal circumstances (over what a given developer can be responsible for), to show that TDD is flexible enough for real-world application.
  • The test doesn’t test anything yet. The execute function also doesn’t do anything yet. The real action happens on line 12 of the CalloutJob.cls. On that line, the job enters the job runner’s queue, and once started the job will succeed or fail in its mission to try to start a call out. Everything else is just context and support structure. So far, line 12 line is commented out. We’re not ready for the functional code to do its thing because the test shell isn’t ready to tell us if CalloutJob.cls line 12 is working yet. None of the assert statements are populated, so we don’t know what really happens when the code runs.

There are test method shells covering…

  1. Single record positive and negative tests
  2. Bulk positive and negative tests
  3. A NULL value test

I’m sure there are more tests we could add for edge cases, but the point is that if we run all those assert statements, we’ll know a lot about the performance readiness of the functional code.

Next, make sure the test fails 

If your test passes before you write any code, test runs might produce false positives.

This is where the critical thinking above comes into play. I need to populate these test class shell method assert statements with something that gives me real insight into the performance of the functional code. But what? If I haven’t taken the time to truly understand my goal and have clear success criteria, I won’t be able to proceed (unless I throw an irrelevant assertion in there like “Does 1=1?” which violates Lee’s 2nd Rule of Apex).

Remember what your function’s one job is

In this example, the main point of this code is to run a call out to an external system when a queued job runs. In this particular testing framework, we stub out call outs in tests, so I’m not actually going to examine the content of the call out response in any method I’m testing. That would be covered by a separate test of the processor class that handles the response (and could also be crafted using this process).

What I really want to know is:

  • The job ran
  • The call out was requested

This will require updating the test class to assert the following: The test passes if the run status shows an attempt to make a call out has happened. Period.

 Remember ISB: Inevitably Something Breaks

Time for a quick confession:I want to test more. The biggest challenge I face when trying to conform to a “rules-as-written” TDD process is my natural tendency towards scope-creep. I don’t want to spend the time and effort to create modular code. I don’t want to just test if the job ran a stubbed call out error free. I want to test that the result the call out brought back looks good. And then test if that result matches the parameters required for the next part of my business logic chain. All those things need tested and will be tested… by other tests. A test that checks if the response from a service is compliant with the parameter requirements for another method is a business logic test. If we are doing TDD, then that test is NOT to be combined with the test to see if the job runner hits a snag trying to start the call out. But I’m lazy. I don’t WANT to write this many tests. I just want to test the end result.  Then I tell myself: “Self… if I know if that end result is OK, the entire process MUST be OK”. Well, maybe. But, self, even if that’s true, this is an article about enterprise code readiness. And when inevitably something breaks, having loosely-couple arrangements of finely tested services backed by finely tested functions means we can identify the root cause of the overall process breakdown (and deploy resources to fix) much more quickly. The repair can this way be localized, and disruptions caused by repairs minimized. Your customer service manager will thank you for using TDD.

Back to the example…

Below is the “failed” result the test produces at this point for a single-record, positive-result unit test. I’m not going to post the failure of every unit test here (you get the point, they fail).

Notice that the failure centers on the assert statement in the unit test. That’s important. The test doesn’t fail because of an ancillary problem. All things being equal, the code works right up until it tries to do the one thing it’s supposed to do. When we test the assertion that the function does its one job successfully, we find it does not. Now we know we have a good test. If we can change our functional code to pass it, it really means that code really is capable of doing its thing.

Since the function’s main point, which, as stated above, “is to run a call out to an external system when a queued job runs” is currently not being attempted, its non-attempt is generating an error as we are expecting the job status to read “Completed” and the status is actually null

Updated Test Class
1
2
3
4
=== Test Results
TEST NAME                                    OUTCOME  MESSAGE
───────────────────────────────────────────  ───────  ────────────────────────────────────────────────────────────────────────────────
CalloutJobTest.testNullCalloutJob     System.AssertException: Assertion Failed: Expected: Completed, Actual: null


Write the code

Skip to next section reading just for TDD and not for Apex at all

In order to pass my test, I’m going to make a call out to the service and send the request that has been prepared for this unit test. I am passing unique configuration to meet the pass / fail criteria of each of the CalloutJobTest class’s five unit tests (including parameters to make the process fail so we know the limits of our method’s parameters), but we’ll just focus on just this one single-record, positive-result unit test. Notice line 12 shows: Callout.toService(new List<Request>{ req });

CalloutJob.cls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
publicclassCalloutJob implementsQueueable, Database.AllowsCallouts {
  //this request will be available later on when the job runs
  publicRequest req;
  publicCalloutJob(Request request) {
    //we will keep this inner class representing the callout on deck until this job runs
    this.req = request;
  }
  //execute callout when job runs later
  publicvoidexecute(QueueableContext context) {
    Callout.toService(newList<Request>{ req });
  }
}

The next section

Thanks to the clarity and context provided by setting up my test, I now know exactly where to focus my efforts. Now, imagine starting here at this step (“Write the code”). Imagine you had done none of the previous work. How much less would you know about the functioning of this code?

To write this article, I picked code I would be familiar with so I could focus on writing about the process and not the code, and I still found improvements to make when I was structuring the solution because I knew exactly what I was trying to accomplish. To a developer, TDD equals raising your own goalposts.

Make sure the test passes

Test Result
1
2
3
4
5
6
7
8
9
10
11
12
13
NAME                    % COVERED          
──────────────────────  ────────────────── 
EnqueueCallout          100%
CalloutJob              100%
Request                 85.71428571428571
Response                85.71428571428571
Callout                 83.87096774193549
CalloutJobTestMockCall  100%
=== Test Results
TEST NAME                                    OUTCOME  MESSAGE  RUNTIME (MS)
───────────────────────────────────────────  ───────  ───────  ────────────
CalloutJobTest.testSinglePositiveCalloutJob  Pass              132

Wrap-Up

And there you go. A vanilla, plain, cardboard example of the TDD process as it can be applied in the real world of a working dev. My goal here was to show a real life example of someone using TDD (obviously imperfectly) so that other developers can benefit from it. Did you learn anything from this article? Anything you disagree with or question? If so, leave a comment below. I’d love to hear about it.

Credit Kelly Tomblin – Editor