3

While I do see the benefits of IoC containers (as I've used them a fair bit), I don't see how they can be incorporated within TDD unit tests (note, I'm only interested in unit tests here, not integration tests).

When I TDD, I refactor constructors to use IoC, so that I can inject fake dependencies wherever I might need to. Implementing a container implies that I'd be deviating from the red-green-refactor-repeat loop and adding code that wouldn't be covered by my tests.

Now let's say that you somehow (with great design prowess) managed to hook in a container in your TDD life-cyle. You certainly aren't meant to create instances in your unit test by resolving dependencies, as strictly speaking, that turns it into an integration test (bringing in multiple production components).

So my questions are:

1) In what scenario might you need a container while unit testing within TDD?

2) Assuming a valid scenario for (1) exists, how would you go about incorporating a container without breaking away from red-green-refactor-repeat?

I should clarify that by 'need', I'm talking about a stage you'd get to where manually managing DI gets tedious because you have a massive object graph.

IMPORTANT: I'm not asking about containers for your test. I'm asking strictly about production containers. I have a feeling that an IoC container cannot be implemented in a TDD life-cyle without breaking away from red-green-refactor-repeat (rgrr) and managing the container would have to be done in a sort of parallel way, if that makes sense.

8
  • 2
    Re: your edit. From a TDD perspective, the way you instantiate your objects (i.e. whether you're using an IoC container or not) should make no difference whatsoever, unless the way you're instantiating your objects is what you're unit testing. Feb 11, 2018 at 22:06
  • 2
    It follows that, if you're not testing the way you instantiating your objects, then you shouldn't use a DI container, since it adds additional complexity for no additional benefit. Testing involving a DI container could arguably be considered integration testing anyway. Feb 11, 2018 at 22:37
  • @Robert Harvey Yes, it definitely turns it into an integration test, no question about that. I think I didn't make it clear that the tests under TDD I'm concerned with are unit tests. I'll edit my question to reflect that. But given that I'm after 'unit' tests, does my statement about the construction/item-registration of a production container having to be done parallel to RGRR make more sense?
    – Ash
    Feb 12, 2018 at 6:17
  • I'm of the mind that IoC containers are absolutely irrelevant to TDD, unless it's the container and its behavior that you are testing. The manner in which classes are instantiated has no effect on their behavior under test. Consequently, what you do with your container while you red-green-refactor is irrelevant. Feb 12, 2018 at 16:25
  • @Robert Harvey That's more or less what I feel as well. If you want to turn this into an answer, I can accept it. One thing that I would like you to describe in the answer is how an irrelevant procedure can co-exist with strict RGRR, i.e, how does one step outside this cycle to construct/manage the container.
    – Ash
    Feb 12, 2018 at 22:32

4 Answers 4

1
  1. In what scenario might you need a container while unit testing within TDD?

I don't see how IoC containers factor into this. Even if you were taking a pure DI approach where you write explicit methods to instantiate your components (which in turn chains to all of that component's dependencies' methods), your question would remain the same: how do you test those pure DI methods?

Whether you use a container or a set of prebuilt Pure DI methods that instantiate your objects for you, it serves the same purpose, and you would write the same test for it.

However, as I'll elaborate in this answer, I don't quite agree that you need to write tests for either of them.

note, I'm only interested in unit tests here, not integration tests

This sort of undoes your argument, or at least shifts the goal posts a bit. Unit tests don't care about a chained stack of dependencies, specifically because they are unit tests.

Any issues in wiring the IoC container will be spotted by your integration tests, since those tests are built specifically to test how your individual components are wired together.

To put it another way, a bad IoC registration doesn't in fact invalidate the units that you test themselves, and therefore their unit tests shouldn't fail.

Implementing a container implies that I'd be deviating from the red-green-refactor-repeat loop and adding code that wouldn't be covered by my tests.

There's two approaches here. The pedantically correct one, and the reasonably practical one.

The reasonably practical approach

Most devs I've worked with, including myself, tend to consider the configuration of the DI container to be startup logic, which falls outside of the application logic scope, which is what your tests cover.

Startup logic tends to be trivial, and is the runtime equivalent of linking projects at compile time. It's not the target for unit tests, because it's "meta logic", in the sense that it doesn't contain business logic but rather the setup logic for the framework in which you will be housing your meta logic.

My reasoning for not testing this is that these tests wouldn't exist to test a behavior, but they are just a "write it again" exercise to confirm that the expected object graph is the same in both your production code and your test.
I find it a waste of time for the same reason that I hate "confirm your email address again" textboxes, and I always end up copy/pasting the email address from the first box into the second one, which defeats the purpose entirely.

Another counterargument here, though slightly outdated nowadays, is that some older IoC setups used config files to configure their container, and you can make a reasonable case here for not needing to unit test values from a config file.

The pedantically correct appraoch

But if you choose to read 100% coverage as 100% coverage of everything, nothing is stopping you from writing tests that confirm the IoC container has been hooked up correctly - at least to the point of not throwing exceptions about missing dependency registrations (the specific behaviors are better tested using more concrete integration tests).

In essence, you could consider the wiring up logic as a unit in and of itself, and thus write tests for it to confirm that it correctly wired your object graph.

You certainly aren't meant to create instances in your unit test by resolving dependencies, as strictly speaking, that turns it into an integration test

If I create a container, register one real dependency (i.e. the one I'm unit testing), and then register a bunch of mocks, then I'm not turning my unit test into an integration test.

There's little purpose to using such a custom built container for the purpose of a unit test (though I can't guarantee that it's never useful), but it doesn't inherently turn your unit test into an integration test.

1
  • Good answer. I pretty much agree with everything you've suggested. Bearing in mind that when I had asked the question, I was still in the early stages of working with IoC containers and TDD. I look back on this question now and it seems redundant but hopefully it will be of use to those starting out :)
    – Ash
    Feb 24, 2021 at 2:39
9

I can do without IoC containers, TDD or not.

I can work with IoC containers, TDD or not.

I can make a grand mess of things with IoC containers, TDD or not.

IoC containers come in a great many flavors and are sold a great many ways. After mastering them and mastering doing without them I've only identified one thing they do that is hard to do on your own. They move construction into a different language.

Rather then just construct your graph of persistent objects in main they have you compose the object graph in some different language like xml or json. This drives home the idea of separating construction code from behavior code. This is a good thing but if you're disciplined enough to do that without training wheels the IoC container isn't getting you much.

But, if you are going to use IoC containers, for whatever reason. then first be sure you're not spreading it all over the place. A IoC container that shows up once in main is no more an anti pattern then an abstract factory. If every darn object knows about it directly, well you've created a Service Locator. No really, I don't care how popular your IoC container is or how much you paid for it, if you use it that way then you're making a mess. Keep it in one tidy little place where nothing else knows about it and what you have is a factory that pops out object graphs as defined in that screwy xml. This is not the anti pattern. This is fine.

If you want to use TDD while using a IoC container used this way you're fine because the object you're testing doesn't even know the IoC container exists.

Maybe you have some obsessive need to test whatever the IoC container exists in. If you're doing it right then that shouldn't be very many things. But fine. If you must test this too then remember that the IoC container is itself a dependency that can be injected and stubbed. My gut says if you need to test this then you're doing something weird but even if you are then you can still do TDD with a container. Unless you're not just doing something weird but something very wrong.

Remember, when you test something it's your job to give it what it needs in a reliable way so that if other stuff breaks, the test still passes. This test should only fail when the unit under test fails. Bear in mind that a "unit" might be one object or an entire object graph. Stay true to whatever boundary you set for your test. Make the boundary clear so when the test fails people know where to look.

If that boundary must include some dopy little IoC container then so be it. If it doesn't, but you are stuck depending on it, at least give the test its own independent container.

But that's just a pain in the ass. I much prefer it when most of my objects don't even know a IoC container exists. When they'd don't I can test them without one just fine. What they don't know can't hurt them.

I should clarify that by 'need', I'm talking about a stage you'd get to where manually managing DI gets tedious because you have a massive object graph.

A massive object graph will be tedious to deal with manual or not. If your unit under test really cares about the whole graph and you just don't feel happy making the whole graph by hand without the IoC container then fine. Use one to build what the test needs. But the test gets its own independent instance of the IoC and it's config info (unless that right there is what you're testing). That isolates the test and makes it independant.

IMPORTANT: I'm not asking about containers for your test. I'm asking strictly about production containers. I have a feeling that an IoC container cannot be implemented in a TDD life-cyle without breaking away from red-green-refactor-repeat (rgrr) and managing the container would have to be done in a sort of parallel way, if that makes sense.

If A depends on B and T want's to test A then T needs some Bish thing to build A with. T could get the Bish thing from a IoC container C or it could just make the Bish thing on it's own. Either way the red green refactor cycle is fine.

Neither A nor B even know about C so T can use whatever C thing it likes or no C thing at all to get it's A and B. If you can't say things like this you aren't doing the Inversion of Control part of your IoC container right at all.

The classic red green refactor cycle: You write T before you write A. You're red. You write A and feed it some Bish thing (that should already be under some other test) using a C made just for this T. You're green. you refactor and A changes. T is still green.

Refactoring Against The Red Bar gives us formal steps for refactoring a working test:

You want to refactor T because you decide you don't need C or want a different C. So you change T. Now it's green but untrusted because we've only seen it pass. We've never seen it fail. We've never tested the test. We messed up. Let's go back to the end of the classic red green refactor cycle above where we have A under test T which has been seen to both pass and fail. What we should have done, to refactor how T uses C, was this:

  • Run the test T
  • Note the green bar
  • Break the code in A
  • Run the test T
  • Note the red bar
  • Refactor T to change how it uses C
  • Run the test T
  • Note the red bar
  • Un-break the code in A
  • Run the test T
  • Note the green bar

That's refactoring against the red bar. It lets you refactor tests just as formally as you refactor code under test. I first talked about it here. Using that you should be able to force your IoC container to dance to the tune of TDD just fine.

24
  • 1
    To be fair, it doesn't have to be in a different language. Most of the decent containers let you register interfaces against the class implementations you want to use, and as long as your constructors take interfaces as parameters, it all just automagically works. Some containers allow you to decorate your classes with marker attributes to show the container your desired mappings. Neither of those techniques requires you to leave your programming language. Feb 11, 2018 at 3:20
  • 1
    In any case, I'm not sure that I see any value in using DI in Unit Tests, unless you're trying to test the container itself. Feb 11, 2018 at 3:27
  • 3
    Any source file that I can look at and tell what framework you're using is as dependant on that framework as it is on the language. If C# has some magical way to decouple from that problem I haven't seen it. Standardized annotations help but still often represent my classes knowing more than I want them to know. Feb 11, 2018 at 3:28
  • 1
    Well, you don't need to decorate classes with attributes; that's just a convenience. You can still register your types in most containers using plain old C#. So I don't see escaping out of my programming language as the principal benefit; I see the principal benefit of DI as allowing me to stand up an object and all of its dependencies and all of those dependencies dependencies et al without having any knowledge of the registrations. Feb 11, 2018 at 3:29
  • 2
    @RobertHarvey Ah, so that's written in C#. That's perfectly fine as long as that line is not in either of those source files. I'll take that over a bunch of attributes/annotations scattered everywhere any day. Feb 11, 2018 at 3:38
2

@CandiedOrange answered the Question as to why it's a good idea to use explicit registration and comments provide more detail. I won't repeat that, since I wholeheartedly agree.

If you follow this advice, and do explicit registration in code, testing it is trivial. Even in TDD fashion (which I like a lot), very simple tests and equally simple implementation. Thus my answer is:

  1. Not necessarily. I do not think testing this is very useful in a UnitTest, I think of it as an orthogonal issue. More like a config file. Some integration tests might be useful.
  2. If you think it helps to have unit tests for this, going about it seams simple, if you configure it with code in your main function (as suggested by CandiedOrange). In this case, it's simply refactoring. Replacing manual wiring up with registration and startup code, while keeping your tests green.
2
  • I do not dispute explicit registration, since this is how I've used containers as well. I'm also an advocate of doing all registration in one place. But regardless, you are stepping outside the RGRR bounds because container construction and registration has nothing to do with what you are testing. If you're unit testing a Person class let's say, you are most likely testing its behaviour, including how it interacts with its dependencies. You are certainly not testing how it's constructed. Please edit your answer with a counter example if you think I'm mistaken.
    – Ash
    Feb 11, 2018 at 10:22
  • Also, just to clarify what you mean by "testing this" and "unit tests for this"...are you talking about unit tests for the container itself? Because that's definitely not what I'm asking here.
    – Ash
    Feb 11, 2018 at 10:35
2

The IoC container becomes useful when part of your software has to instantiate various classes based on runtime information.

For example MVC web controllers must be instantiated when a request comes in which matches the route for the controller.

So your first red test which pushes you down the container road is : 'When a request has path X, Controller Y is created with all its required services'

You might start off writing a hard coded controller factory to make the test pass, but as you develop more and more controllers with different requirements at some stage you will want to refactor into a more generic factory which will resemble a container.

Given that you know such things as containers and generic factories exist prior to starting your code. It's not unreasonable to skip those steps and jump straight into IoC containers.

Proponents of TDD would argue that its very design pushes you towards 'good' code with these advanced structures.

I have a feeling though, that if you are very strict about writing the least amount possible to make a test pass, the heat death of the universe might occur before things like object orientated code and IoC containers 'evolve' from TDD.

Just assume they will eventually and skip those steps.

2
  • Which steps are you referring to when you say "skip"?
    – Ash
    Feb 11, 2018 at 21:06
  • iterations of TDD write test, write code, refactor
    – Ewan
    Feb 12, 2018 at 10:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.