The term is unfamiliar to me -- is it related to "fault injection"?
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.
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.
The alternative to dependency injection is for functions to instantiate dependencies internally rather than having them be passed in from the calling context.
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.
it makes the code cleaner and testable.
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.
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.
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.
As for unit tests... I mostly add them to projects with something egregious happens or a very hard bug to spot can occur - just to prevent anyone else from foot gunning themselves (here be dragons or whatever).
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?
You don't need to test the call to /api/foo, you only need to tell 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.
See also: Frontend development that requires running the entire stack in order to test it.
For example, the business logic is what talks to the database. This way, I can test the controller separately from the business logic. Often, I don't even bother testing the controller, since it is usually just a single line call to the business logic.
Anyone who writes a lot of tests realizes very quickly that in order to write testable code, you have to separate out the layers and compartmentalize code effectively in order to allow it to be easily tested. Tracking dependencies is critical to good testing (which is where this whole DI conversation got started).
If you aren't writing code like this, then you're just making testing harder on yourself... and then we end up with tons of code which can never be modified because the dependencies between layers are all too complicated and intertwined. Don't do that.
At this point in my 27+ year career, I don't see any reason to not do things correctly. The patterns are all instinctual and no need to try inventing something new, I don't even think about it any more, I just do it from the start.
https://github.com/gaffo/CSharpHotChocolateZeroQLIntegration...
Still playing around with the right level for this but this is currently nice as it gives me compiled type checking and refactoring. This is an example/extraction from another project which uses react as the client. Wasn't big on cross language api level tests yet for speed of development, as that's a tradeoff. Redundancy vs even more framework.
When I code review, I try to make sure I call out "fake tests".
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.
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.
I think it’s reasonable to disavow expecting single unit tests for nearly every method on every class with mock/boilerplate/copy-paste hell. However, I would still expect “local integration” unit tests that exercise broader chunks of code and keep code coverage up.
So it should be “I don’t need to write a new unit test for this because an existing unit test calls the method that calls this method without it being mocked and you can see it’s covered in the code coverage.”
Or do you mean specifically "expecting" interactions with those mocks? Because I agree that's usually not that valuable.
Ideally, service owners would write and maintain the fake to ensure that it stays in sync as changes are made to the actual service.
But it isn't quite as smelly when I see a mock (or "stub" more typically) used as an expedient way to create a fake to pass in as a dependency.
Like you said, it is asserting on the interactions with a mock that is what primarily smells bad to me. I very rarely (honestly I think maybe never) see this done in a way that isn't just rewriting the implementation of the method as expectations in the test.
Over time I have favored very basic unit tests and leaning in more on automated integration and regression tests. I know the theory of unit tests catching things earlier, I just don’t think it matters in practice.
Of course it's not necessarily a full replacement, but it's definitely better and more time efficient than bad tests.
I'd actually say: Not in the slightest. A type system just rules out illegal input values[0] but it won't ensure that your business logic is correct.
And even that[0] is not entirely correct because, most of the time, a type system just rules out some illegal values but not all of them, because it cannot represent all possible constraints. Gary Bernhardt discussed this whole type checking vs. tests debate at length here: https://www.destroyallsoftware.com/talks/ideology
In many cases mocks are now over used where previously they were important in say 2008. Especially now with languages that support functions as objects, better generics, and other features which weren't common a while back. Likewise frameworks are and languages are generally way more testable now which means you're doing less backflips like static injecting wrappers for DateTime.now into libraries to make tests work. This further allows more contract and less implementation specific testing.
As with most things there is a lot of nuance/art to doing it well and smoothly
When programmers really embrace dependency injection like the parent comment, the implementation largely is the inputs, and your code expects them all to be implemented correctly (or mocked correctly by test code). I completely agree that mocks are overused, and their importance in 2008 were in my view a direct result of the popularity of dependency injection patterns.
Following that logic, the best way to remove mocks from your tests is to minimize dependency injection, relegating it all to a single place in the code wherever possible, and implementing all other domain logic as pure functions of input to output. How to test that code which touches other systems? Integration tests (or multi-system tests or whatever you like to call them).
In 2008, I actually found code often got more testatable when it was dependency injected. I spent a month ripping singletons out of a gui application so we could make it into a web service, that would have been a lot easier with a DI model. The system was nigh-on untestable until we got rid of those singletons.
I link it in another comment but https://github.com/gaffo/CSharpHotChocolateZeroQLIntegration... is how I do things these days, api integration test, and more focused unit tests where you don't understand the library well yet, there's lots of tricky edge cases, or error conditions such as setup that are harder to integration test. I'm still feeling it out.
(Which IIRC is what I changed it to)
Thinking about the original TDD approach -- red, green, repeat maybe it's not that bad? I prefer your approach, where you actually verify the state change. But in the absence of that, I wouldn't mind the "call the mock" approach. At least it does change some of the contract?
I suppose having lots of mocks is a smell after all..
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.
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.
The abstract concept of OOP (messages between complex objects, as defined by Alan Kay) is an attempt at mimicking biological systems. Most modern languages implement data abstraction, but call it OOP, where they encapsulate some functionality with the data it operates on. Really helped with varying data formats in the AirForce in the 60s, apparently. There isn't anything wrong with this abstract concept either - it's a way of structuring a solution, with trade-offs.
Support for unit testing and mocking has little to do with OOP, and everything to do with the underling platform. Both C++ and Java, for example, do not have a special runtime mode where arbitrary replacement of code or data could occur. This is necessary for mocking functionality that is considered implementation detail and hidden by design. The hidden part is great for production code, not great for testing.
For example, if an object in java has a field like 'private final HttpClient client = new CurlBasedHttpClient();' this code is essentially untestable because there is no way in Java to tell the JVM "during testing, when this class instantiates an HttpClient, use my MockHttpClient".
Kotlin fixed some of that with MockK, which can mock the constructor of a Kotlin object, and you can return your mock implementation when the constructor is invoked.
Clearly, it's a platform issue. There could be a world where you could replace any object in the stdlib or any method or field with a mock version. JavaScript is much more flexible in that regard, which is why unit testing js code is much easier.
The root of it all stems from the fact that unit tests need to change some implementation details of the world around the object, but production code should not be able to, in order to get all the benefits of encapsulation.
If you get rid of modern OOP, you are swinging the pendulum in the opposite direction, where your tests are easy to write on any platform, because everything is open and easily accessible, but your code will suffer from issues that creep up when structures are open and easily accessible, such as increased coupling and reduced cohesion.
I stand by my statement that OOP is totally unnecessary.
Edit: This is more blunt than I intended it to be. For what it's worth, I happen to agree with you in principle, but I also think you're taking the roof off the car here and boldly claiming that the experience is so much better in the summer. What about winter? What about when it rains? What are the trade-offs? Not mentioning the downsides means that you either haven't found them, haven't thought about them or are intentionally omitting them from the discussion.
To dismiss the whole of OOD/OOP so cavalierly just goes to show they don't know what they are talking about.
Much of the success of Modern Software is directly due to the wholescale adoption of OOD/OOP in the last few decades.
Separation of Concerns, Modularization, Reusability, Type Hierarchies, Type Composition, Interface contract-based programming, Frameworks etc. were all made mainstream by OOD/OOP. These are things taken for granted by programmers today. As somebody who has been doing OOD/OOP since the early nineties i can tell you it was the single biggest reason for the explosion of Software in the past few decades.
As a concrete example, early in my career i had programmed in C using the Windows API; both 16-bit and Win32 (Thank you Charles Petzold). It was difficult, tedious and a lot of work. And then Microsoft introduced MFC (Microsoft Foundation Classes) Framework with Visual C++ IDE. With a few clicks of the wizard, i had a complete skeleton application with a lot of hard work already done for you. That was a revelation for me on the power of OOD/OOP. Things i had slaved over in Win32 was now at the fingertips of every noob who could type. The same revelation happened (but not to the same extent) when i moved from Xlib to Motif on Unix platforms.
I had pointed you to Bertrand Meyer's book OOSC2 (in my other comment) as the book to read to understand OOD/OOP. Another great book to study is Barbara Liskov and John Guttag's Program Development in Java: Abstraction, Specification, and Object-Oriented Design.
I was formally trained and cut my teeth in OOP code bases and my latest company we are very light on classes. 99% is plain objects and modules that operate on them. Everything is very straightforward and logical. In my side projects I’ve stopped using classes as well and it’s so much cleaner.
This is opinion and preference. If I hired a guy who wanted to litter the code base with AbstractUserBeanFactoryFactories I’d have to let them go because it’s a situation I don’t want anymore.
This is putting the Cart before the Horse. Computers, Internet etc. can't do anything without the Software to drive them. That software is written in some Language/Tool using some Paradigms and Software Engineering principles. The ease of use, ease of structuring, ease of understanding etc. of these are what drives Creation, Adoption and Expansion of Computing and Devices.
>I was formally trained and cut my teeth in OOP code bases and my latest company we are very light on classes. 99% is plain objects and modules that operate on them. Everything is very straightforward and logical. In my side projects I’ve stopped using classes as well and it’s so much cleaner.
If you think merely using a syntactical structure like "class" (and Design Patterns) is what makes code OOP, then you don't understand OOP. You can do various degrees of OOP without language syntactical support. This is why i listed the principles and not some syntactic sugar in my previous comment. It is a way of thinking and Software Engineering which has given the most "bang-for-buck" so far in the Industry.
>This is opinion and preference. If I hired a guy who wanted to litter the code base with AbstractUserBeanFactoryFactories I’d have to let them go because it’s a situation I don’t want anymore.
Opinion and Preferences must be based on facts else it is only as good a "Flat Earther" category and nothing more can be discussed.
Class s: fun a(self)... fun b(self)...
The problem is mutability not how state is bound to a function.
Though it’s not always practical, like when needing to push an element to an array for making updated state. It is nevertheless possible to shave of pure parts considerably making mutability easier to maintain.
Similar, OOP is an obfuscated way to run a function with a context. The first time you separate the data in the context from an object, it’ll be hard to get it right and make it easier than to just use Objects and Methods. But once you’ve done it two or three times, it’s as easy as breathing.
You can optimize loading the context to be better than copying a bunch of unaligned data in lots of ways, but polymorphism is a common way to get there.
I am a Scala programmer, and our code mixes OO and FP. Almost all of our classes are completely immutable.
In the end, you will always need to encapsulate data and functions in some manner. The FP approach involves module systems, but as I understand it, objects in Scala actually provide a better module system than found in pure FP languages.
The few times I had to use Java afterwards I felt the same - all OOP features were unnecessary or at least didn't feel like the most straightforward approach.
I always hated testing and I still do, but every time I commit to doing it right I catch so many errors before QA.
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.
Just vanilla C++ classes, and virtual interfaces if we need to mock things for unit tests.
No automatic wiring of the hierarchy.
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).
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.
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.