18 ms·
Unit testing and using dependency injection to write test-able code. I'm not sure if it was years, but it wasn't immediate. I just didn't understand why depend
by bsuvc 4y ago
Unit testing and using dependency injection to write test-able code.
I'm not sure if it was years, but it wasn't immediate. I just didn't understand why dependency injection was good at first, and not just someone's weird personal code style choice.
I thought it was just people being "Enterprisey" which I'd encountered many times over the years.
Once I committed to unit testing, I realized how necessary it is.
Unfortunately I still encounter customers who haven't bought into it and as a result have untestable code. It's so hard to go back and retrofit testing.
- everybodyknows 4y ago> dependency injection The term is unfamiliar to me -- is it related to "fault injection"?
- la_fayette 4y agoNo it is a design pattern to structure code. It is also known as the hollywood principle (don't call us, we call you). Meaning that dependencies of a class are provided from the outside, the class itself doesn't know how to create instances of dependencies, just relys on the dependency container (also named inversion of control)...
- ngc248 4y agoIt is a pattern where you inject the dependencies of a class into it rather than create the instances of the dependencies within the class. for more info --> https://en.wikipedia.org/wiki/Dependency_injection https://en.wikipedia.org/wiki/Dependency_injection it makes the code cleaner and testable.
- everybodyknows 4y agoWhat I find troubling about that page is that it does not qualify up front whether the dependencies are: A. Also depended upon by other, coequal unrelated classes, possibly maintained by others, OR B. Depended upon only by a single class (of higher functionality) Situation A is sometimes called a portability interface; B might be called an internal structuring interface. The difference has crucial implications for social power relationships between the human developers and users involved.
- azeemba 4y agoLet's say Class A depends on Class B. A lot of people have the instincts to have A construct B so that the callers of A don't have to worry about it. But this makes testing A in isolation difficult. When testing A, you want to mock out B with an instance the test can manipulate. So we want A to not create B, instead we want B to be "injected" into A. The general strategy of having B passed into A is called dependcy injection.
- k__ 4y agoThe idea of DI is that you creat resources indirectly, and don't call the named constructor in your code. You then use interface types to interact with the resource. This way, your code isn't bound to a specific implementation. Some DI frameworks even go so far and define all resources in a config file. This way you can switch out the implementation without a recompilation.
- deleted 4y ago[deleted]
- gavinray 4y agoGoing to steal a description I wrote for a blogpost several years ago, when I had recently understood DI for the first time so it was very fresh in my mind: https://hasura.io/blog/build-fullstack-apps-nestjs-hasura-graphql-api/#dependency-injection-di https://hasura.io/blog/build-fullstack-apps-nestjs-hasura-gr... Dependency Injection solves the problem of when you want to create something, but THAT something also needs OTHER somethings, and so on. In this example, think about a car. A car might have many separate parts it needs: class Car { constructor(wheels: Wheels) {} } class Wheels { constructor(tires: Tires) {} } class Tires { constructor(rims: Rims, treads: Treads) } class Rims { constructor() {} } class Treads { constructor() {} } We can manually construct a car, like: const car = new Car(new Wheels(new Tires(new Rims(), new Treads()))) But this is tedious and fragile, and it makes it hard to be modular. With dependency injection, it allows you to register a sort of "automatic" system for constructing an instance of "new Foo()", that continues down the chain and fetches each piece. class NeedsACar { constructor(@Inject private car: Car) {} } And then "class Car" would have an "@Inject" in it's "constructor", and so on down the chain. When you write tests, you can swap out which instance of the "@Injected" class is provided (the "Dependency") much easier.
- TillE 4y agoI know the car is a classic, cliche OOP example, but for any semi-experienced programmer, I feel like it's a really bad choice of analogy which only serves to obscure how you would use a technique like DI in actual real-world code, rather than an artificial toy example.
- jacobsimon 4y agoI think you might have it backwards. The first example _is_ dependency injection, too, since you are passing the required instances into the constructor. The non-DI approach would be for the Car class to import and instantiate the Wheels class inside its constructor function.
- sanderjd 4y agoThe first example is also dependency injection. The rest of what you described is why a dependency injection framework often becomes useful in a system with a large dependency tree. The alternative to dependency injection is for functions to instantiate dependencies internally rather than having them be passed in from the calling context.
- okino 4y agoGood series about the approach from a functional perspective https://fsharpforfunandprofit.com/posts/dependencies/ https://fsharpforfunandprofit.com/posts/dependencies/
- rozap 4y agoPeople would have way fewer opinions on it if it was just called "passing stuff in"
- Simon_O_Rourke 4y agoI think the poster is referring to Guice or something similar - that there's a framework which selects which instantiated runtime object gets injected as a dependent into another, thereby automatically "figuring out" what object needs to get instantiated first, and then next etc.
- dymk 4y agoYou'd do fault injection by injecting dependencies that respond with / throw faults.
- itronitron 4y agoDependency injection is one of those technical terms that are made up to describe something that has been done for decades but with a new name because the creators of the new term don't have much experience and/or they want to gain notoriety for creating a new programming technique. If you've ever written a constructor for a class that has arguments (within the constructor signature) that are used by the instance of the class when instantiated then you have done dependency injection, or put more simply 'passing stuff in' which was eloquently stated in another comment on this thread.
- kagakuninja 4y agoI believe the term Dependency Injection was coined by Martin Fowler, as the meaning of the pattern name Inversion of Control is less obvious. I'm going to assume that Martin Fowler has quite a bit of experience, although you might accuse him of wanting to gain notoriety... In any case, DI is not at all a new trendy term. Inversion of Control dates back to the gang of 4 patterns book from the '90s. Yes, constructor parameters are one way to implement DI, but not the only one. I'm ambivalent on the whole pattern movement, but they are a great educational tool for novice programmers, and it is good to have some standard terminology for core patterns. To put it another way, constructor based DI is not simply the use of constructor parameters, it is understanding the OO design principle that side-effecting code should be isolated into objects, rather than just dumping a bunch of DB queries into your functions, like we did back in the stone age.
- number6 4y agoCan you explain the concept? I feel I didn't grok it fully
- ericlewis 4y agoNot sure about poster above, but I found a large amount of value in writing tests when developing API backends. I knew the shapes and potential data. Was easier to write tests to confirm endpoints looked right than manually hit the api endpoints.
- latchkey 4y agoAlways amuses me when I see tests spin up a http server, just to call a function.
- pintxo 4y agoWhat specifically amuses you? Todays http servers may have any number of request altering/enhancing "middleware" calls between the incoming request and the actual business logic/function. How do you ensure that your api works as designed if you only test (pure) business functions? Or do you re-create the middleware chain of functions manually for you test?
- ericlewis 4y agotechnically this would be considered an integration test I think.
- latchkey 4y ago/api/foo -> fooApi() You don't need to test the call to /api/foo, you only need to test the call to fooApi(). It doesn't/shouldn't require a http server to do that. Just call the function directly. If you want to test that /api/foo exists, that is essentially a different test and only requires a mock version of fooApi(), because you've already tested fooApi() separately above. The benefit of this approach is that your tests run a lot faster (and easier) not having to spin up an entire http server, just to test the function. As for the middleware... that is also tested separately from the business logic. You don't want to tie your business logic to the middleware, now do you? That creates a whole dependency chain that is even harder to test.
- closeparen 4y agoSeven years into my career I'm increasingly convinced that the emperor has no clothes with respect to unit tests that are just transcripts of the code under test with "mock.EXPECT()" prepended to everything - 95% by volume of the unit test code I've ever read, written, or maintained.
- bsuvc 4y agoYeah, the mere presence of unit tests is not enough. It has to actually assert something useful. When I code review, I try to make sure I call out "fake tests".
- closeparen 4y agoThe useful assertion to be made about gateway/repository layer code is that it gets the expected behavior out of the dependency. This is not an assertion you can make when you've mocked out the dependency. You must make it in an integration test, not a unit test. Unit tests in these layers just make assertions about "it calls this method on the client" or "it sends this string," which tells you nothing about whether doing that is actually correct. It's relatively uncommon for handler/controller code to have logic worth testing, most of the time it's just maintaining the separation of layers and concerns by wrapping gateway/repository calls. All there is to assert about it is that "it calls this function in the next layer." Every once in a while there's nontrivial functionality to test in the middle, and unit tests can often be a good fit for that, but in my experience it's more the exception than the rule.
- lscharen 4y agoThat’s a great point. Seeing lots of mocks and assertions that certain functions are called is often much ado about nothing since no actual code functionality is exercised. I do sometimes see the return value functionality of mocks used as a stub, just because the dev hasn’t internalized the distinction and can “make it work” with a mocking library. One of the only legit use cases for mocks that I have personally come across is validating things like a sequence of API calls to an external service, or queries to a database where there is a check that certain efficiencies are guaranteed, e.g. verify that an N+1 select problem doesn’t creep in, or knowing that a app-level caching layer will prevent redundant API calls.
- monero-xmr 4y agoAnother solution is to eliminate classes and only use structs or similar plain objects. Makes mocking and testing functions much easier. At this point I see no reason for OOP whatsoever and consider it a big mistake.
- adlpz 4y agoI agree with OOP not being needed at all, but I don't agree on this being an alternative to dependency injection. However you structure it, there will always be "glue code" that ties the nice inmutable code with the outside-interacting bits, and if you want to unit test those, dependency injection (with functions or state, not classes or instances) is still the way to go.
- mrloba 4y agoTrue, but there might not be a big need for unit testing the glue code. By its very name it doesn't contain much logic to test. Integration tests are more valuable in this case.
- sanderjd 4y agoThis seems totally orthogonal to me. Using a totally non-OOP functional style, you can either instantiate state within a function or pass it in from the calling context, which is the same trade-off that dependency injection targets.
- monero-xmr 4y agoIf you work on a legacy code base with tens of many millions of lines of code, the non-OOP will be a better code base. My opinion.
- sanderjd 4y agoI wasn't disagreeing (or agreeing) with that opinion. It's just orthogonal to whether or not dependency injection is a useful approach.
- Traubenfuchs 4y agoI‘ll add to that: the difference between "offline" unit testing and spring integrarion testing with test containers and real application contexts + all the related concepts like @SpyBean, @Mock, @MockBean... I always hated testing and I still do, but every time I commit to doing it right I catch so many errors before QA.
- groundcoffee 4y ago+1 to this. It doesn't help that some dependency injection frameworks' (ahem, looking at you Dagger2) error messages can be convoluted and hard to understand.
- lamontcg 4y agoI keep looking for a reason to write software using a proper DI container framework. In talking to Java devs most of them just mention injecting mocks for your database. You don't really need a full DI container for that you could use a service locator instead or some even lighter DI pattern. If you have that particular hammer though and know how to use it, then it becomes easy to achieve and use config to inject your backend. But that's a long way from glorious full DI containers where you never call 'new' in your code anywhere and all object creation can be dictated by config. I suspect that must be only the kind of thing that people who maintain 1,000,000 line codebases that are at the center of massive bureaucracies.
- mrfox321 4y agoAt my bigco, there's no service-level DI (c++) Just vanilla C++ classes, and virtual interfaces if we need to mock things for unit tests. No automatic wiring of the hierarchy.
- lamontcg 4y agoI'm thinking more like programmers working at the IRS or SSI or something like that, where you have million line long decades old codebases. Not anything that Google or Amazon would write. Applying a DI framework right from the start may actually be a best practice--that allows the codebase to evolve under the next 10 years of contract programmers--instead of just being the YAGNI that it would to anyone in the tech sector. Although you might say that microservices are like a distributed DI framework that would let you rewrite or mock components provided that the rewrite/mock adheres to the API framework (curiously, I've actually seen that used in tests with a quick and dirty in-memory sequential unauthenticated server used for testing clients where the server passed the same API test suite as the real server).
- rr808 4y agoAgreed, this has taken me years. It also is tough because lots of people over use it IMHO. When the logic keeps bouncing around dozens of classes its too hard to follow, even if its easy to test.
- jjice 4y agoThe thing that made it really click with me was the quote (two whom I'm not sure it belongs to) that is roughly "code to abstractions/interfaces, not implementation". The idea that I take advantage of good abstractions and I send those objects into my classes that need to perform actions via those abstractions made a lot of sense. Helps enable good polymorphism, as well as unit testing and other things. I don't think I'm doing it justice, but the idea took a good while to understand there reasons behind it. Some books that helped me grok the idea were - Patterns of Enterprise Application Architecture - Clean Architecture - Architecture Patterns with Python along with running into problems that could be easily solved with a decent abstraction at work and learning to apply it directly.