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.

Ash
  • 219
  • 2
  • 6
  • 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. – Robert Harvey Feb 11 '18 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. – Robert Harvey Feb 11 '18 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 '18 at 06: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. – Robert Harvey Feb 12 '18 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 '18 at 22:32
  • To put it simply, if you write a class, and you use TDD to write that class, it should still work when you put it into a DI container. – Robert Harvey Feb 12 '18 at 23:21
  • Yes, but the issue isn't it still working, the issue is stepping outside RGRR to put it into the DI container. – Ash Feb 13 '18 at 23:04
  • And what I'm saying is that your DI container is an orthogonal concern. It doesn't participate at all in your TDD process. You don't test your Continuous Integration in a TDD context either, but you still have it. You don't test your database server in a TDD context either, but you still have it. Stubs, mocks and the `new` keyword take the place of your container during TDD. – Robert Harvey Feb 14 '18 at 16:45

4 Answers4

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.

candied_orange
  • 102,279
  • 24
  • 197
  • 315
  • 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. – Robert Harvey Feb 11 '18 at 03:20
  • @RobertHarvey those decorators are building a different language in the same source file. In my view that's even worse. Java with Spring annotations isn't Java. It's Java/Spring. When you have to mention it when you advertise the job you're fooling yourself if you think this is still plain vanilla Java. Anything you spread everywhere your language is is part of your language. – candied_orange Feb 11 '18 at 03:24
  • Ah, well maybe Java sins that way. Attributes in C# are considered first-class components of the programming language and framework. I could see how Spring annotations could be seen as "bolted on." – Robert Harvey Feb 11 '18 at 03:25
  • DI isn't even the only way you can do this. XAML in WPF is really nothing more than a way to stand up an intricate object graph using XML. Of course, there's some other nice things about it like Data Binding and MVVM, but fundamentally that's all it really is. – Robert Harvey Feb 11 '18 at 03:26
  • 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. – Robert Harvey Feb 11 '18 at 03: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. – candied_orange Feb 11 '18 at 03: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. – Robert Harvey Feb 11 '18 at 03:29
  • 1
    I think you're talking about the convention over configuration paradigm which is good so long as you don't let the convention start leaking into your names. Again, if I can look at a classes source file and tell what container you're using your class is dependant on it. If my little every day POCO's have no idea how they're managed then I can manage them as I see fit. – candied_orange Feb 11 '18 at 03:33
  • 1
    No. Not convention over configuration. Explicit registration. Example: `container.Register()`. If you use a container `Interface` and write an adapter against it (like [Prism](https://github.com/PrismLibrary/Prism) does), you can do DI against the interface and have no idea what container it uses. – Robert Harvey Feb 11 '18 at 03:34
  • 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. – candied_orange Feb 11 '18 at 03:38
  • 1
    I suppose the reason for using attributes is that explicit registration is no longer required. So long as you use the same container, you can use your classes in multiple programs and never have to write a registration routine again. I haven't decided which way I prefer yet, though in our current project we're going with explicit registration because it's all in one place. – Robert Harvey Feb 11 '18 at 03:41
  • 1
    "So long as you use the same container." Tell me that wasn't the idea of the containers marking department. I favor explicit registration in one place. Removes the distraction and some day you may need to change containers. It'd be nice if doing so didn't mean touching every file in the code base. – candied_orange Feb 11 '18 at 03:43
  • @RobertHarvey plus it's nice to sit down with a class and for a moment forget the container exists. – candied_orange Feb 11 '18 at 03:51
  • @CandiedOrange It seems as if your idea of an IoC container relies heavily on implementation details. Moving construction into a different language is certainly not what their benefit is. And as Robert Harvey has clarified, in C#, this isn't the case anyway. But that's besides the point. The point of this question isn't to dsicuss the benefits of IoC containers (I know what they are...). – Ash Feb 11 '18 at 09:18
  • @CandiedOrange All I'm asking is if there's a need for it in TDD, and if so, how would one incorporate it in TDD without breaking away from red-green-refactor-repeat (rgrr). I have a feeling you can't, and that implementing an IoC container would required to be done in a sort of parallel way to rgrr. I should however expand a bit on what I mean by 'need' though, as that is context dependent. – Ash Feb 11 '18 at 09:20
  • IoC and TDD are note mutual exclusive. I can follow (and I do) TDD methodologies while working in Spring projects (90% of the cases). I have a hard time understanding your concerns. Maybe your IoC container is not the proper one for your needs or maybe your design is not taking in count the inversion of control. That's not bad. Maybe your designs are perfectly fine with pure DI. Hard to say without knowing how your design is. – Laiv Feb 11 '18 at 09:53
  • Additionally, not every single project needs IoC conatiners. As Candied as told, these introduce a heavy dependency on frameworks that will do the heavy lifting for us. We can minimize the impact not abusing of the framework's features. But then what's the point of using framework? Have you considered to don't use IoC conatiners? – Laiv Feb 11 '18 at 09:58
  • @Liv I'd be really interested to know how you manage this, even if it's a reference to a blog post. If it demonstrates clearly with an example how one conforms strictly to TDD while also incorporating an IoC container, then I can accept the answer. – Ash Feb 11 '18 at 09:58
  • @Liv Perhaps it might be useful in describing my motivation a bit. I've worked with IoC containers heavily in a project that didn't use TDD. So everytime a new class had to be added, it was really easy to just register it in the container and have it automagically manage its construction. Saved a lot of time with manual construction (this to me is the main benefit in using a container). I have also been involved in TDD projects where DI is obviously used to help assist with mocking. I value the benefits each one provides, but I can't seem to figure out how to bring the two together. – Ash Feb 11 '18 at 10:01
  • @Laiv Sorry there was a typo while tagging you in comments above. – Ash Feb 11 '18 at 10:41
  • @Ash better now? – candied_orange Feb 11 '18 at 15:00
  • @CandiedOrange Not at all, sorry. You may remove everything before the bit where you've quoted my important note, as all that's to do with either benefits of IoC or how to TDD, neither of which I've asked for. You've then missed my question about 'how' you incorporate a 'production' container into your RGRR loop, whereas what you've described is a test making use of some already existing container to resolve some Bish thing. – Ash Feb 11 '18 at 21:13
  • @CandiedOrange Also, it seems as if you are using a container to resolve dependencies in your test. This makes it a test container, not a production one, and I've specifically asked for prod and not test containers. If on the other hand your container C is indeed a prod container, then the way you are unit testing is completely and utterly wrong. You shouldn't be resolving dependencies via a prod container in a unit test as this then becomes an integration test. – Ash Feb 11 '18 at 21:20
  • @CandiedOrange Furthermore, the fact that you think massive object graphs are tedious to manage even when they are not manual is disturbing. That's the whole point of not managing them manually...so that they are not tedious. Perhaps the way you are using IoC containers is not right. If you think moving construction to a different language is the greatest benefit they provide you with, then you've clearly missed their purpose. I've dealt with massive object graphs; with a container, these are piss easy. – Ash Feb 11 '18 at 21:24
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.
Bruno Schäpper
  • 1,916
  • 2
  • 14
  • 24
  • 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 '18 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 '18 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.

Ewan
  • 70,664
  • 5
  • 76
  • 161
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.

Flater
  • 44,596
  • 8
  • 88
  • 122
  • 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 '21 at 02:39