Unit testing can sometimes feel like a daunting task, especially when we encounter poorly designed code or lack experience in testing. But fret not! In this article, we’ll look into how we can structure our unit tests and how to think about unit testing to keep positive and stay on track.
Hey there, fellow developers!
Picture this: your team’s just finished up implementing a new feature, and you’ve been assigned to write the tests for it. You’re all pumped up to write those unit tests and get it out to the world. The pressure’s on, you don’t have a lot of time, and the project manager is asking when it’s going to be finally finished and pushed to production. It’s all on you now. “No problem”, you think, “I’ll have this done soon enough!”
Then you take a look at the code you’re testing and you realize that the code is a complete mess. The bulk of the functionality seems to be in some class that you want to test, but it looks like it’s trying to do everything and involves a ton of other classes. It just has so many dependencies injected into it, and they all interact in strange and seemingly unrelated ways. “How on earth can I ever test this mess?” you scream inside your head.
Now at this point, you’re probably blaming yourself wondering just why you can’t quite wrap your head round it all. And kind of beating yourself up mentally that you’re just not quite “getting it”. That’s what we tend to do as developers — we’re great at blaming ourselves!
At that point, and at this point too, it’s a good idea to step back, take a breath and think about how we write tests. So to make our lives easier, there’s a structure we can use which let’s us break down the unit testing process into well-defined sections — this is the Arrange-Act-Assert structure.
The Arrange-Act-Assert Method of Writing Unit Tests
So Arrange-Act-Assert, sometimes AAA (or “triple-A”) for short, says that all unit tests should be broken down into 3 discrete sections:
- Arrange
- Act
- Assert
By sticking to this structure, you’ll always have a rock solid go-to method to create your unit tests from. You just ensure that each unit test has these three parts. So let’s see how it works and what each part needs to do…
The Arrange section
In the Arrange section, we set the stage by preparing the objects and dependencies needed for our test. In this section, we can prepare the inputs needed — i.e. any parameters required as input to the method we’re testing, and also we take this opportunity to set up the different objects that our method will interact with. We want these objects to interact in a way that makes sense for our specific test scenario. Think of it as arranging the perfect team for a mission.
Now after the “arrange” section, you should be in a position to invoke the actual method you’re testing — in other words, you’ve prepared all the input needed, and set up how any mock objects should behave during the test. That’s the goal of the “arrange” part, so with that in mind, let’s move on.
The Act section
Next comes the Act section, where we unleash the action! Here we invoke the method we’re testing and, well, see what happens! Now during this method invocation, the system under test (the class you’re testing) will take that input you set up in the previous step, and it will interact with its dependencies (so-called “collaborators”) exactly as it would do in production (because you’ll have set them up in a way that allows you to test a specific scenario). For example, you might get one collaborator to throw an exception, simulating that data isn’t found. Or maybe you get another one to return a bogus value. You can also get them to behave nicely too. So you’ll usually end up having a set of tests which test both negative and happy path scenarios. We won’t get too much into it here, but just know that you’re in control: inside any given unit test, you’re setting the stage for how those collaborators will act when that method being tested is invoked.
Remember to stick to one method per test that you invoke though. Since we don’t want to confuse ourselves or others when something goes wrong. Keep it simple and focused. In this way, when a test fails in our build, we just have a single specific method to look at, instead of having to search through multiple method calls and not knowing which one caused the issue.
The Assert section
Ok back to our AAA structure then… Now, it’s time for the Assert section. This is what comes after the method’s been invoked. Here, we check if everything went according to plan. It gives us an opportunity to take stock of the “state of the world” after the method being tested has been invoked. We can compare the actual outcomes with our expected outcomes and see that they match up (specifically, make “assertions” that assert that those things we believe should be true after a test, are actually true). So like looking at any output variables to see if they hold the values we expect, and seeing that those collaborators were acted with in a way we expected — for example checking that a dependency that was injected in actually made a specific call (like a repository class actually doing an insert into the database in a happy path test where all input was good and the test was set up in the arrange section to behave nicely and emulate a happy path scenario, where everything — all those collaborators — work as expected.
The “assert” section is your opportunity to be the detective and investigate the “scene of the crime” after the fact. You’re looking for clues that indicate that the test went well and was successful: is the state of the world as we expected it to be after the method did its thing? If not, something’s up and that gives us clues as to where to look (by seeing which assertions failed for example when the test fails in the build)!
What if I still can’t write the tests?
When theory is not enough
Now, it’s great having a framework to use to write tests — theory’s good because it gives us a way to work. But even armed with the Arrange-Act-Assert structure, we might still find ourselves in a testing nightmare. Going back to that huge class we mentioned earlier on. It might seem like it’s impossible to test: all those dependencies being wired in, with all those interactions — do we have to mock everything out? How do we even do that? It might seem as if you just don’t know where to begin.
Don’t worry, it’s not you — you’re not to blame!
Now at this point, as we said earlier, the instinct is to look inwards to yourself and feel inadequate. You think: well I know all this stuff about how to write tests, but this is just too complex. But, with a class like this, trust me — it’s not your fault. It’s the code. You see, sometimes, we encounter classes that are as tangled as a bowl of spaghetti. They have tons of dependencies wired in, and it’s just a mess. You simply can’t test classes like this. There’s too much going on and it’s just too much to get your head around.
Refactoring is your friend
So, how do we escape this unit testing nightmare? Well, we need to refactor that code, my friend. Break it down into smaller, well-behaved objects. Give each object a clear purpose (you may have heard of the Single Responsibility Principle — or SRP — that’s what we’re alluding to here and make sure they only interact with a few dependencies. Think of it as decluttering your codebase and creating a zen garden of testability. Once you have a cleaner design, and work with classes that only inject in a handful (say 2 or 3) dependencies, and those classes individually work towards a single goal, cohesively and exclusively (i.e. they’re not trying to “save the world” and do everything in the system), then you’ll find that it’s then easy to test them: you now have clean code and clean code is inherently testable.
Similarly, whenever you get in to a flow where you’re writing tests and it seems pretty easy, it’s because you’re working with this clean code: you’re working with nicely testable classes where it’s clear what they do, and this makes testing an easy, almost trivial and mechanical “we’ll just crank out some tests” kind of activity.
You see, by refactoring and embracing proper object design, we pave the way for smoother unit testing. Our tests become more manageable, less prone to breaking, and a joy to work with. We’ll sleep better at night knowing our tests have our back, catching those pesky bugs before they wreak havoc in production.
Final thoughts
So just remember if you ever feel frustrated when writing unit tests: the struggle you face in writing unit tests is not a reflection of your skills. It’s the code that’s to blame. But fear not, my friend. With the right mindset and a dash of refactoring magic, you can conquer any unit testing challenge that comes your way.
So, go forth, refactor that code, and write those tests like a boss. Unit testing is your superpower, and together, we’ll build robust and reliable software. Happy coding!
💥 Want more? Go get the FULL COURSE here: 👉 JUnit and Mockito Unit Testing for Java Developers 💥
►► Grab our FREE beginners guide to Java if you’re completely new to Java!
👉 Check out our courses at https://courses.javaeasily.com/courses
👉 Listen to our podcast at https://spotifyanchor-web.app.link/e/xjAOXisFABb
👉 Visit out our website at https://javaeasily.com/
👉 Read our articles on Medium at https://medium.com/java-easily
You can also join in the conversation by connecting with us at our Facebook page or alternatively follow us on Twitter!