Disclaimer: The following is written on the basis of what has worked for me in the past. This is not intended to be a formal or exhaustive description. Use it as you see fit, I do not take any responsibility if you screw it up! 🙂
Hi, I’m Diego and I have several years (I prefer not to say how many, but let’s say “enough”) working in Salesforce. I am also an Agile enthusiast and lover of applying related techniques throughout my work.
I’ve found test-driven development (TDD) can be a great technique for building robust software, but when we work in Salesforce, I find some constraints or restrictions can make it frustrating. In this post, I’m going to show when and how I use TDD while coding in Salesforce.
Let’s start at the beginning:
What is TDD?
TDD is an Agile development technique that requires you to write a failing unit test before writing any “production code.”
How are you supposed to do TDD?
First I’ll describe how TDD is done in general (this is the way to go in languages like Java).
- Write a unit test and make it fail (a compilation error is considered a failing test). Write no more lines of code than needed.
- Write the least possible production code lines to make the test pass (fixing a compilation error is considered a pass test).
- Refactor the code.
- Repeat until you are done.
Let’s check out an example in Java so you see how it works. In this example, we wanna create an advanced calculator of integers.
Let’s write a failing unit test:
Oops, MyCalculator is not defined yet, compilation issue…therefore, it is a failing test.
Let’s make the test pass:
Compilation problem fixed! The test is passing again. Woohoo!
No tons of code to refactor.
Let’s continue with that test to make it fail again.
Mmm…getOpposite is not defined, ergo compilation issue, ergo failing test.
Let’s fix that. Let’s write the minimum code to fix the test:
getOpposite is defined and returns 0 to any parameter (in particular, 0). Test is passing again!!!
We still don’t have much code to refactor, but there are some name changes we could use to make code easier to read ( yup, yup, yup…unit test code is code, too).
Much better now! 😀
Let’s add a new minimum test to fail.
Right now, getOpposite returns 0 to any parameter… it’s a fail!
Let’s write the minimum code required to make the test pass.
Yay! It’s green again! Let’s continue.
Let’s add a new failing test.
Last test fail (we are return 0 to any value different than 1), so now we need to write the minimum code to fix this test:
Test is passing again… but this solution is not good, let’s refactor.
Tests are still passing and we solve all the cases! We are done! Well, not actually, we still need to document, test more, write more tests and write even more tests…but we’re on the right path.
I expect this silly example gives you a feel for what TDD is and how it is done.
Now, let’s continue with the discussion, focused on Salesforce.
- Code coverage: We get great code coverage without even thinking about it.
- Testability: The code is designed to be testable by default (we are actually testing every time we change something).
- Easier to refactor: We should not change or refactor code without having a set of tests we can lean on. As we are constantly writing tests that we know are able to catch bugs (we make it fail at the beginning), we know that we have a set we can rely on.
- “Better” code: We are constantly refactoring the code, striving for the best possible code.
- Predictability: After we finish a “round,” we are completely sure the code is working as we designed it to work and we know we didn’t break anything. We can say we have “working software.”
- Prevents useless work in Salesforce: In Salesforce, aside from Apex, we have plenty of options to make changes like triggers, workflow rules, process builder, etc. Imagine that after we write a test that changes a value on a contact record, it passes. We could discover that there is another moving part that is taking care of that change (or we wrote the test badly).
- Documentation: Tests are a great tool to communicate with other developers (or the future you) how, for example, a class API should be called and the expected results of each case.
- Overtrust: It happens to me that, as we are testing continuously and we are getting test green, I sometimes have a feeling that the code is working perfectly…but it doesn’t mean it is. We may miss, or simply get lazy, and leave a case out of the unit test.
- Slow in Salesforce: TDD is designed based on the theory that compiling or writing a test is really fast (a jUnit unit test has to run in less than 1ms). In Salesforce, we need several seconds to compile (the code is compiled on the server) and several more seconds to run the test. In my opinion, this is usually 10+ seconds. As we are compiling and running tests constantly, we add several minutes of “waiting for Salesforce.” However, this can be mitigated if you think you will need to write/compile/execute tests later anyway – you might as well do it upfront.
Me when I realize the QA found a case I had not considered when I was doing TDD
I will (probably) use TDD when…
In general, I’ve found that TDD is a great tool in several circumstances and I tend to do it almost always in the below cases.
- Back-end bug fixes: Doing TDD in this context has two big advantages. First, you make sure you are able to reproduce the bug consistently. Second, and even more important, as you are writing a test specific to the bug, you know you will never introduce that bug again.
- Back-end work with clear requirements and a clear implementation strategy: In this context, writing tests is going to be easy and implementing the production code will be easy, too, as you know where you are heading when you create the test cases.
- Back-end work with clear requirements and minor implementation unknowns: In this context, the test is easy to write and the production code may be getting clearer as you move into cases.
- Back-end work with some requirements discovery: Imagine in our calculator example you write a test to divide by zero and you realize you’ve never discussed that case with the BA. TDD helps you discover opportunities to clarify requirements.
I might do TDD, but it’s unlikely…
- As part of requirements discovery: You could write unit tests as part of requirements discovery, and discuss it with your stakeholders, BA, or other technical people, but you probably have better techniques to support this process.
- Front-end work: I’m gonna discuss this briefly later, when we talk about Lightning web components.
I will never do TDD when…
- I’m doing a prototype: By definition, a prototype or PoC should be discarded after we show it, so I code it as fast as I can, focused on demonstrating the core functionality.
- I’m experimenting: If I’m trying a new idea, I don’t focus on code quality (again, this is a prototype).
- I’m evaluating implementation options: There are some cases where you want to compare two implementation options, so focus on having a good-enough-to-decide prototype and throw it away after you decide…then do the stuff well.
- I don’t care about code quality: I know code quality is not usually negotiable, but in very limited and extreme situations, it may not be the top priority. For example, when everything is screwed up on prod and you need to fix the problem ASAP because the company is losing millions of dollars per minute. In this very extreme circumstance, fix the problem as fast as you can, make your company earn money again, go to sleep (or maybe get a drink) and tomorrow at 10 am (yup, after a stressful night, start working a little later the next day) make the code beautiful with TDD. Make a test that reproduces the bug and then fix and refactor the code properly.
Me again, but on one of THOSE nights.
- When creating test code is extremely difficult (but not possible): In Salesforce there are a few elements that are very hard to test, like working with CMT. In this scenario, I’d probably split the problem into two parts – one that is TDD-doable using mocking data (@TestVisible is your best friend here) and a second, smaller part that I’d consider how to test later (if I even consider it).
How I do TDD in Salesforce
I really don’t do TDD as I defined at the beginning of this article when I’m working in Salesforce. Why? Mainly because of the slower compile/test flow, but also because in Apex we generally start writing integration tests instead of unit tests. Instead of “regular” TDD, I tweaked the formula a bit to work better under Salesforce circumstances.
- Write an entire deployable test that checks the flow or use case. Yup, I said deployable, so if I called a method I haven’t created yet, I will create it, empty, so I can deploy.
- Run it and get a failure.
- Write the minimum code that makes that test pass.
- Continue with the next flow or use case.
- When I’m done with all the flows and use cases, I refactor the code again (splitting methods, checking code cleanliness, documentation). I run the unit test continuously, every few changes to check if everything continues to work as expected.
To make everything clear, let’s view an <could-be-real-world> example.
As a user, I want the values stored in any object copied into a number of specified contact fields. The specified “mappings” will be stored in a CustomMetadataType called Contact_Mappings__cmt. The Contact_Mappings_cmt has two fields:
- Original_Fields__c Text
- Mapped_Fields__c Text
As I said before, I should start writing an Apex test that tests a business case. The first thing I’m thinking of developing is “The contact should not change if there is no mapping defined.” I have to write a deployable test that is going to fail with the minimum amount of code to make it fail:
As expected, the code deploys but the test fails. So, we need to fix it! We can simply return the same object.
Now It passes, but we don’t have a lot of code to refactor (we could extract some constants in the test).
This is a much better test.
Test still passes!
Okay, let’s add another case. What if we check that the User.LastName is copied into the contact when I define the Mapping Lastname => Lastname? Great idea, let’s do it!
I start to write the unit test but…. I realize I can’t do an Insert in a CMT. Or, I give seeAllData permission to the test and define it in the project metadata. Or, I have to somehow deploy it.
Remember that I said that I don’t do TDD when writing the test is extremely hard? Well, it looks like I’m in one of those situations. At this moment, I can quit writing this blog post and go cry…or I can redefine what I am developing with TDD, leaving all the complexities outside of scope. I imagine you would be very annoyed after reading this far to see me just quit, so let’s go with the second option.
I can’t use the CMT right now, so let’s do something different. What if we use a Map<String, String> where the key is the field in the original object and the value is the list of fields names in the Contact Object. It might work, later on we just need to read the CMT and create a Map with that information, but spoiler alert…that won’t be covered in this article.
But okay, great, let’s create a map and write the deployable failing test.
And as it was expected… it fails.
Let’s write the “minimum” code that makes that test pass
Our new test passes, but the other one failed! Let’s fix that.
Let’s do some refactoring, either in test or production code.
I think the put/get part is messy to read (and has its own meaning), so let’s split it into its own method.
Also, as we want that theMap could be injected into test case scenarios, the @TestVisible annotation is useful here.
Now we should add a new test that executes a new flow and see it fail. I think you got the idea, so I won’t do it now, but just to specify the cases, I can think:
- Mapping a field to multiple fields (separated by colon)
- Does nothing if origin field is wrong
- Does nothing if destination field is wrong
- Does nothing if types are not compatible
…and so on
Can we do TDD in Lightning web components (or front-end)?
The short answer is yes, we can.
Long answer: As the Jest test can’t see the objects, but they see only the “generated DOM”, it may be harder to do TDD in an efficient way for front-end solutions. Usually, it is better to test visual code by watching the result and THEN write the tests we need to ensure code won’t break in the future.
TDD is a best practice that’s good to master so that you can decide the best moment to apply it (I don’t believe there is One Ring to rule them all, One Ring to find them, One Ring to bring them all, and in the darkness bind them, thank you J.R.R. Tolkien). Applied correctly it will make you produce better, more robust code…and fewer bugs, which means…
Homer is happy