A few of us enjoy the process of writing unit tests. It feels great having a safety net that verifies that your production code is working as expected, or that the changes you just made to pre-existing code didn’t introduce any regressions. Even better if those tests are quick, and you can rerun them again and again to keep getting that dopamine hit whenever you see that green bar that indicates that everything is all right.
But for most of us the prospect of writing tests is an awful proposition. We dread finishing our work but still not be fully “done” because now we have to spend that unpleasant time writing those vile tests. It feels like a chore, something you know you have to do, but you never want to do.
But why is this the case? It is just writing code after all, and we all love writing code (I hope). So, if we love writing the production code, what makes writing the test code different? Why do we enjoy one so much but hate the other?
Everyone is different so we all have different reasons to dislike doing something. For all I know a unit test bullied you when you were a child and that’s the reason you hate and fear them so much. But there are some reasons that, in my opinion, pretty commonly make people not want to write tests.
Difficult to write
The first of these reasons is that tests can be difficult to write. Difficult can mean several things in this case. It could be that they are actually hard to write because the system under test (the functionality you are writing tests for) is really complex, so you do have to think long and hard about how to test it. Or it could be that they are rather simple but a chore (no much thinking, but a lot of writing and setting things up).
Slow
Another reason why I think people don’t like tests too much is that, without actively paying attention to how long they are taking, they tend to become slow, and slow things, specially when we are talking about computers, are definitely not fun to deal with.
They Feel pointless
And finally, but probably most importantly, writing unit tests can feel pointless for a lot of us. Why write a unit test if I just tested the real system myself and verified that everything is working fine? It definitely feels like a waste of time knowing that something works and having to write a test that verifies what I already know.
Through the rest of this post I want to go through these different reasons and see if there’s anything we can do about making tests not be such a pain to write. But I want to present real, actionable things that you can do while writing tests (and regular production code) to make things easier. Again, we are all different and have our different reasons for liking or disliking something, but even if you never really truly like writing tests, maybe these techniques can help you at least tolerate it.
Let’s start with identifying what makes it difficult to write tests.
I find it better to look at a code example that we can go through so that we can dissect it and line-by-line, identify what might make it difficult to write a test for.
Let’s imagine we are writing a system for a supermarket checkout counter. Our system is in charge only of the things that happen in the counter, namely:
Let’s look at the code.
Hopefully everything is clear, but here is a recap of what the code does in plain English:
You might be thinking that this code looks great or you might think it is awful. I personally think this code has a few problems, and I almost want to go in there and start refactoring before we begin writing the tests. But that actually takes me to Tip #1: Do not refactor prematurely.
The reason why we don’t want to start refactoring right now is that we are pretty sure this code already works. We wrote it, then we went through the UI and we tested it manually and everything was ok. If we start refactoring right now to make the code easier to test, and then we break the functionality, what’s going to happen is that we will HATE writing tests even more. We will think that writing tests is a liability that causes your perfectly fine little princess working code to break because you have to touch it to make it testable.
The last thing we want is for you to hate testing even more, so right now JUST DO NOTHING to your production code. Leave things alone.
With that in mind let’s get right to it and start writing some tests for this. We want the process of writing tests to be as fun as writing production code (or at least to not be an awful time), so what I’ll do is that anytime that I feel like I’m not having fun, I’ll stop, and see if we can figure out why that is and if there are ways for me to go back to having fun again as soon as possible.
First thing I’ll do is that I’ll create a test class for our functionality, I’ll call it SupermarketCheckoutCounterTest.
So far so good, I’m having fun.
So… where should we start? 🤔🤔🤔. Ok, I’m not having fun anymore. I have no idea what to test first.
The reason I’m not having fun is because at the beginning the problem is too large to tackle. So, Tip #2: Don’t think too much.
Thinking too hard will leave you exhausted at the end of the testing session, so that will just give you the impression that testing is hard. The process should be quick and painless, always making progress towards the end goal by taking very small (and hopefully fun) steps.
But not thinking can also be hard. How can I progress if I cannot think?
The good news here is that the code will tell you what to do. You don’t even need to read the code, just look at the code.
So, let’s take a birds-eye view of the system under test once again (don’t read it, just look)
I learned this trick from this excellent talk:
Testing and Refactoring Legacy Code
Highly recommended.
That red section at the top is pretty much just setting variables and doing some extra queries, but it doesn’t really contain any kind of business logic that we want to test. The meat of what we want to test starts with the if statement, so that’s where we’ll focus our eyes.
From there we see we have different levels of indentation (which are caused by control flow statements, like ifs, fors, whiles, switches, etc. What we need to remember is that the deeper the code, the harder it is to get there. Because our Tip says that we should avoid thinking as much as possible, we’ll start with the shallowest level: the green bar.
Now we know which test to write first: check that an exception is thrown when the customer is on the express lane with more than 10 items.
So let’s define that test and start creating the data that we want to pass to the method we are testing.
We know that we want to use the EXPRESS checkout type, so that’s easy.
We want a counter Id, so let’s insert one and get the Id, same for the customer, same for products….
I don’t know about you but I’m not having fun anymore…
The moment we started needing to communicate with the database and setting all of that complex stuff up, it just killed all of the momentum we had going.
So let’s stop. And let’s make it fun again. To me something that’s more fun than having to set up a lot of data by hand, is just being able to ask something to set things up for me. That takes me to Tip #3: Automate setup data.
Unit tests are made out of 3 phases, usually called Arrange Act Assert or Given When Then. The “Arrange” part of the tests are 99.9% of the time, in my experience, the less fun thing to write. So this means this is where most of our love needs to go so that we can make it fun.
In this case I need a bunch of Ids, so I’ll create an IdGenerator class that I can reuse (kudos to fflib_IdGenerator which is from where I took the original Id generation logic). Note that having to create this class is a little bit painful right now, but that is only because this is a brand new project and I don’t already have this. Now that we have it, imagine how much we can reuse this same helper FOR THE REST OF TIME after just having to spend a little bit of time writing it now.
Whenever I need an Id I don’t need to setup an SObject, insert it into the database, make sure I’m not running into validation rules and triggers, and then extracting the Id out of it. That sounds like a nightmare. Now my test can just do this:
Ok, that to me was kind of fun. We now have a reusable helper that can help us with every single test we write in the future and just had to spend like 2 minutes writing it. But…the test doesn’t pass. Hmm, back to no fun. What’s the issue now?
The issue is that there is a pretty big smell in our code. We have too many input parameters. The method we are testing only has 2, which sounds perfectly fine, but that doesn’t paint the whole picture. Let’s look at the code again
Our code actually has 9 dependencies!!! Yes, there are only 2 input parameters in the method, but then we have 2 more in the constructor, and on top of that we have 5 more implicit dependencies: every query that we are doing to set up our method is essentially an “input” for the rest of our business logic.
We want to have a passing test as soon as possible, and we also want to think as little as possible. To accomplish this we will do a few things that will feel a little bit like cheating, but to get clean we gotta get dirty first, so let’s get dirty. We are going to bend Tip #1, and do some refactoring. But this will be so painless and consequenceless (is that a word, who knows? But you know what I mean) that I’ll say we are still within the spirit of the rule.
If we look at the code that we are testing, we only care about one of the queries, the one for products. Everything else we don’t care about, so we will move into the if so they are out of our way:
This is called, ladies and gentlemen, a hack. But that’s fine, because this hack just made us realize something important: This query code doesn’t belong to this class. Why is this code painful? Because it is speaking a different language than our system under test. We are testing the logic of going through a supermarket checkout, which is business/domain logic, so why are we speaking query language?!?!?
If we go to our local supermarket right now and ask anybody working there what happens when you checkout, they will tell you: someone scans your items, they charge you money, someone else bags your items. That’s our domain language. That's what our domain experts understand. No one will mention a database. No one will say "SELECT FROM BLAH BLAH". If someone said that to me in the supermarket I’d get out of there as fast as I could and call the cops. So why are we doing that in our code? We shouldn’t. We are talking 2 different languages (domain language and SOQL language), that’s confusing.
For now, we will just move the code that speaks a different language (in this case SOQL) out of the way and put it aside in the helper method.
Now that our system under test is ready to be tested again. Now we have deeper knowledge of our domain, which leads me o Tip #4: Treat test code as production code.
We just learned a lot about our domain, we know what kind of “language” we should speak. No SOQL, no nonsense, speak clearly CHILD! Let’s rewrite our test a little bit and make it speak clearly:
Look at that, isn’t it beautiful? 🥹
I can ask a domain expert (someone working at a supermarket) to read that unit test and I'm fairly certain they'll understand what it means, and if there's anything wrong with the way we are expressing the business logic they can let me know.
What we did is that we moved what doesn’t matter out of the way. Anything that will be shared with other test methods, like the Ids to pass, or the SupermarketCheckoutCounter instance, we move it out of the test method. Remember that in Apex tests run in isolation, so we can move everything to the static context and just keep our test clean and Apex will ensure that our tests keep being standalone.
And now we have a passing test!
Ok, we’re rolling, I’m having fun. We let the code tell us what to test next, which takes us to the next indentation, which is yellow. At this point it seems like we can just test the happy path, so let’s do that.
First let’s extract the rest of the SOQLs out, just like we did for the Products query (hackThePlanet):
And let’s write the happy path test, but just checking one thing at a time, because Tip #5 is: test (and assert) one thing at a time.
At this point I can write a single test that asserts that the whole happy path flow works (scan, payment, bagging, etc.), but remember, I don’t want to think. Let’s keep it simple and dumb and just test and assert one thing. This doesn’t mean we want a single ASSERTION, this means we want to ASSERT ONE THING. Those mean different things.
You oughta know the drill by now. We make our test speak our language and move things out of the way. Our test is looking pretty cool and expressive, but we have one problem: what to assert?
In this test I want to check that the payment went through, and to do that I’ll resort to Tip #6: Test UNITS, and Tip #7: Trust that your dependencies are tested. but I have a pretty big NEW problem. That’s Tip #8: Beware the new keyword (see what I did there? that’s called a pun).
At this point tips are coming at you fast, so let’s slow down and go through each. First thing is that we want to UNIT test, this means that we only want to test the single unit under test. We want to assert that payments went through but NOT test the Payment Processor code. To do this we can simply test that the processor code is being correctly invoked when needed. That’s why the next tip, trust, is important. We have to trust that there is some other code out there that is correctly testing the Payment Processor, because are not going to worry about what it does here. Our code is only dealing with checkout, we don’t care about how the payment gets processed, so we don’t worry about it.
https://martinfowler.com/articles/practical-test-pyramid.html
But finally we have that “new” issue, which is how our Payment Processor is getting called, and that is because we are using the new keyword:
The reason why this is problematic is because right now our code is tightly coupled with the Payment Processor instance. If one of our test reaches that code, it will try to call it, and if that code needs complex set up (for example, imagine if that code also calls the database) that would NOT be fun. No sir.
So we will fix the problem by avoiding the new keyword altogether, and injecting a fake version of the Payment Processor into our unit under test. We will give it something we control, so that it is nice and fun to set up.
This is a 3 step process:
2. To avoid breaking any code, we leave our existing constructor intact, and add a new one with all the things we want to inject:
3. Update the call using new to now use our instance variables, for example, in the scanner we completely remove the line where we were creating a new instance:
This strategy is called Dependency Injection, or Inversion of Control around some circles. Dependency Injection is a pretty broad topic, and I can’t go into all of it just in this article, or it would end up being an entire book.
For now, the only thing you need to know is that the new keyword is a potential for pain during tests, but also can potentially be painful to refactor even when not unit testing. Remember that we are not making the code testable just for the sake of tests, one of the reasons we are doing all of this is because it also makes our code better in general. If our code is testable it also means it is modular, which makes it malleable and open to future changes.
At this point our code is able to receive its dependencies from our tests. But the question is, what to inject?
We could create “mock” objects, either by creating them by using Salesforce built-in mock tools or using a library like fflib_mock . But I don’t want to do that. Instead, I will do it by hand, because I love you and I want to show you that it not that bad!
There is only one thing that is painful about this whole thing to me, and that is that to be able to pull all of this off, we should ideally depend on interfaces (or virtual classes that we can extend), and not concrete objects (note that this is not a problem when using mocking since we can mock concrete classes).
I won’t bore you with steps to create interfaces, so I’ll just go ahead and refactor my code a bit to use interfaces instead of classes. My constructor code now looks as follows:
Long story short I now have an interface for the different classes I depend on, but the real classes are still in there for my production code that was already working in my leaner constructor.
I don’t know about you, but I need a dopamine hit, so let’s run that one test we have and make sure we haven’t broken anything.
Oh yeah, that’s the stuff.
Let’s go back and make some updates to our tests. Now that we have a constructor that can receive injections, we want to make sure to use it:
There’s something I want to point out: The constructor for the class we are testing changed (technically it didn’t change, but in our tests we want to use the new one). Usually when that kind of thing happens (constructor change, method signature change) that makes all of our tests break and that is not fun. But here, because we have the setup code in a helper method, only one place needs to be updated. Fun!
Now, let’s write fakes. You might think this will not be fun, but it will be fun, because Tip #9 is: Master your tools.
I will let my IDE to do the painful work for me:
Here you see me creating a fake for the IProductScanner. I create a new “fake” class, implement the corresponding interface and then just let the IDE autocomplete all of the implementations of the interface for me. 10 seconds max. FUN!
Since we are testing the Payment Processor flow, I’ll do something a little bit different in there. In our fake I’ll add a Boolean flag that will tell us if that code got invoked or not, checkiout:
And with that in place, the final version of my test looks as follows:
That’s beautiful if I say so myself. And let’s run it to get that dopamine hit:
Oh yes.
Notice something about these tests in the top right corner. 66ms. That is 66 thousandths of a second. That’s Tip #10: Keep your tests fast.
Slow tests are no fun. Keep your tests fast by avoiding things that are slow, like calling the database. The fun thing is that, as we discussed previously, setting up database data is tedious and no fun, so by NOT doing that, you also get the added benefit of fasts test. 2 for the price of 1 combo deal.
At this point, to be honest, I’m not even going to keep going. Writing the rest of the tests is simply going through the same stuff over and over again: keep going through the happy path assertions, modify your test setup fakes to cover negative scenarios, once you are done with the indentation level, go to the next one (the last one is red), and you are done.
That’s full coverage of the system under test in only a little over 1/10th of a second. I had fun.
Now, you’ll notice that because of our “hack” with moving SOQL queries around we are not really getting full coverage of the class itself. That’s because we are not really done, we need to fix that hack.
But fixing that hack is a refactoring exercise, and this post is just about testing, so I will not go much into that. What I will do is fast forward to what the end state of the code might look like post-refactoring.
But you know what’s fun? I can now refactor to my heart’s content. I have tests to back me up and warn me if I make I mistake. At this point, before refactoring, I would make sure to commit that code, and then refactor one thing at a time, making sure to run my tests after each refactoring, and if at any point I get a failure, I just go back to the previous state where my tests were all good.
Here’s a potential refactor:
Instead of receiving the Ids, receive create a Request object for the service and receive the pre-queried SObject s directly as so:
The reason this is cleaner and OK to do is because this class belongs to the “service” layer. This is not a class we want to call directly from the UI, instead we probably have a controller class that interacts with the UI, and then calls into this. At the service layer we can trust that the previous layers (in this example the controller layer is our "previous" layer) already conducted validation, queried the data, and made sure that we have enough information to execute the desired operation.
At the controller layer is where we want to essentially “orchestrate” our functionality. Our controller can then take care of calling the different pieces of our application that provides the intelligence to fulfill a user request:
Coding like this keeps everything nice and tidy. Each layer has its own responsibility (querying vs business logic vs UI vs validation) and gives us the added benefit of making everything testable.
This is long, so I’ll just give you the tip(s) (giggity)
If I could summarize this entire thing the entire thing in one tip, what I’d say is: If it gets boring, STOP.