Spring is a great project, it helps a lot with common, usually mundane, tasks. One of the best features is, of course, dependency injection. It is very easy to use, too. For example:
And that’s about it, once you instantiate your application context properly, you’ll get your foo
bean with its dependency injected automatically. When you use Spring Boot, then one simple annotation, @SpringBootApplication
, is all that’s required.
I’ll show how a Spring application can evolve and how Spring’s presence can influence testability. I’ll do it the TDD way, as this will show the evolution of the application quite well. It will also show, how I can start running into problems very early on. I’ll also be using JUnit5, AssertJ and Mockito whenever necessary.
I’ll write a simple application that does foo. Ever wondered what it does? As far as I know, foo is equal to 1
if bar is true, else it is equal to 2
. The details of bar are not important.
First steps - The Spring way
I started with Spring Initializer and I got myself an application skeleton, so I have something I can actually execute, and thus get a working Spring ApplicationContext
. You can check the full source code on GitHub and the commits history will more or less reflect what’s happening in here.
I’ll be spending most of the time in two classes: Foo
and FooTest
. A first, trivial test will probably look like this:
The test looks good, but I have to get this foo
from somewhere. Since I have no clue where it comes from, I’ll just declare it as a test class field
The IDE stopped complaining about the foo
part, now it’s complaining about apply
. Look at the implementation, aka „the simplest thing that could possibly work“:
As expected, when I try to execute the test, I get a NullPointerException. The test has no idea of Spring, ApplicationContext is not started, and there’s nobody to inject Foo
bean into the test. Let’s fix this by adding some annotations to the test:
I also annotate the foo
field with @Resource
to let Spring know I want it injected:
Almost there, but a „No qualifying bean of type ‚com.example.demo.Foo‘ available“ comes up this time. So I annotate Foo
with @Component
:
Now it’s working. Execution time is also not that bad, 71ms on my machine, so I can move on.
My tests are running within Spring ApplicationContext
and can use all of its power. Now I need to verify that, if bar
is false, then foo
returns the 2. Test:
Last time, I cheated and ignored bar
completely. This time, the test looks exactly the same like the first one, but I’m expecting the foo
to behave differently, based on bar
results. That forces me to include bar
and also ensure, that it will be returning what I want. I guess it’s time to invite some mocking framework, like Mockito. Now the tests should look like this:
Also, I need a Bar
class now:
Tests are executing properly, but the second one is obviously failing. Let’s fix it:
Now the tests are green and are taking around 1.45 seconds to execute.
First steps - The Spring-less way
So far, I developed the tests in a fashion that I very often see. Let’s try to achieve the same thing in a different way and then compare both approaches. I’ll be refactoring the code trying to get rid of Spring as much as possible.
Actually, the only thing that Spring currently provides is dependencies. There are two places that use it: bar
is injected into foo
and foo
along with bar
are injected into the test class. The first one we can fix by switching to constructor injection. Later on, this will enable us to inject dependencies manually.
As new versions of Spring can figure out automatically that they should use the constructor injection, we don’t even need an annotation. The tests work now, so we can move on.
To eliminate the injection of foo
and bar
into the test class is actually really simple. We instantiate them manually:
Tests are still green. They stay that way even if I remove Spring annotations from the test class. So, I don’t need Spring for testing any more. Now the whole test class looks like:
If I execute it, it’s all green and done in around 0.66s.
Comparing two approaches
Getting rid of Spring in my tests saved me around 0.8 seconds per run (all times taken from the Gradle output and averaged over a few runs, variation was minimal). It might not seem like much, but please think about what it might mean for any „real“ project.
Currently, my ApplicationContext
is ridiculously small, only two classes. In real projects we might have hundreds or thousands of them. Then, starting the context might take some time, and if we run our tests often, we’d already be wasting minutes every day, just waiting. We could shorten the time by avoiding the initialization of things we don’t need (like databases). But then the tests get more complicated, because we need to control which parts of our application are „real“ and which need to be mocked.
Another issue that is not showing up yet, is cascading dependencies (a common problem with all examples: they’re usually too simple to show a full problem or a solution). Following this path would make our tests depend on things we don’t really need or care about. If some dependencies of our dependencies suddenly change their behaviour, we might find ourselves wondering why our tests turned red, although nothing has changed on our side. Now, our code indirectly depends on much more than we’d like.
Let’s try to see if I can push this little example a little further so it starts showing this second problem.
Adding a second dependency - The Spring-less way
Now my business logic gets slightly more complicated. Before I check if bar
is true, I need to check if baz
is above 10. If it is, I apply the old logic, if not, I return 0 immediately.
This time I’ll start with a „Spring-less“ way. Because the logic has changed, I will need to change at least some of my existing tests. Let’s start with a first one:
I need to provide baz
, so in FooTest
this happens:
I’ll fake it again and don’t even inject baz
into foo
, and the test will still pass.
Time for fixing the second test:
As before, the only changes I made are the name of the test and mocking baz
’s behaviour. Still no changes to production code needed, the tests pass. To finally force myself to use baz
in foo
, I need to write a new test:
This one fails. I cannot run away from adding a new dependency in foo
:
Now, the fix is quite simple:
All three tests are now green and are executing in around 0,7 seconds.
Adding a second dependency - The Spring way
There are honestly no changes to the spring-less way, apart from a few annotations here and there. Both implementation and tests look exactly the same, apart from one small thing:
But that is also no surprise. Tests are still executed in around 1.45 seconds.
Simplifying
Now, I’ll do a small trick on the spring-less code. So far foo
, bar
and baz
were defined on a class level, outside of the test method. I’ll pull all three into my last test method and see if some interesting possibilities will present themselves.
It seems, I don’t need to care about what bar
is doing. Can I remove it completely from the picture? Yes, I can!
Passing in null is OK in here, as I’ll never reach a piece of code, that actually tries to do something with it.
Conclusion
Would it be possible to do such a trick on tests with Spring? Not really. And that is exactly where the problem starts to surface. Dependencies start to creep into our tests. All of a sudden we find ourselves in a situation where some dependency, far away from a piece of code we’re testing, changes its behaviour and affects our tests. In any project with a reasonably large ApplicationContext
, tests written this way now depend on half of the whole codebase. It also means, that in order to run the tests, Spring needs to initialize the whole context, no matter how big it is.
By removing Spring from the picture, we make those problems go away. There’s no more magic around where my dependencies are coming from or how many are there. We have full control over it and can provide only what’s necessary. This also has the additional benefit, that we’re very explicit as to what influences our test.
Having said all that, Spring is not bad and also necessary! Sooner or later, you will need to write a test that starts the whole ApplicationContext
, for example, just to see if it can start. Or to see if you’ve connected a few beans correctly and they can interact with each other. Just not every single test needs to be a Spring one. Unit tests should definitely be Spring-free. Yet I see, time and again, that most tests are using Spring. Make yours and your fellow colleagues' life easier and only use Spring when necessary.
Happy coding!