“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”…
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 requestCallout.cls
– Makes a JSON REST based call out to an external serviceCalloutJob.cls
– Places job in queue for our system job runner to executeCallout.cls
asyncronouslyEnqueueCallout.cls
– Provides access method to runCalloutJob.cls
CalloutJobTest.cls
– Test the performance readiness ofCalloutJob.cls
Take a quick glance at the code, then read notes about it down below…
Functional Code
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; } } |
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(...) } } |
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 ]))}; } } |
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
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. class , new 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. class , new 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. class , new 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. class , new 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. class , new 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 ofCalloutJob.cls"
. ThoughCalloutJob.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 theCalloutJob.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 ifCalloutJob.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…
- Single record positive and negative tests
- Bulk positive and negative tests
- 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
…
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 });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class CalloutJob implements Queueable, Database.AllowsCallouts { //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( new List<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
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