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:
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
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 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
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.
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
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
foo, I need to write a new test:
This one fails. I cannot run away from adding a new dependency in
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.
Now, I’ll do a small trick on the spring-less code. So far
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.
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.