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.
Seven 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.
I call those lockdown tests and they're a smell. You don't want to dictate the implementation, just the inputs and outputs. The former leads to very brittle code, the latter describes and validates the contract. It's also important where you put the logic of the test in the code base when you have multiple layers. This latter part is harder and system dependent.
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
> You don't want to dictate the implementation, just the inputs and outputs
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).
I generally agree with you, DI went really far in many ways through up to like 2015.
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.
Yep, I think mocks are mostly a smell. The "functional core" of a module should be entirely or almost entirely unit testable with (possibly fake) dependencies passed in. The glue code ("imperative shell") should be tested at a higher level - "integration" or "end to end" or whatever you want to call it - which looks at the externally observable effects of running the code (database changes or API responses or whatever) rather than the details of its execution.
Fakes are not mocks. A fake is an actual implementation of the dependency that runs in-process/in-memory. This means that, unlike with mocks, your test code does not dictate the behavior/outputs of the dependency.
Ideally, service/library owners would write and maintain the fake to ensure that it stays in sync as changes are made to the actual service.
Basically what Cyph0n said, fakes and mocks aren't the same thing.
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.
Amen. I have been frustrated at many jobs where developers were so proud of endlessly writing mocks and spending more time on their tests than on core functionality.
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.
I think in practice unit tests are both the most and least useful tests. Unit tests of code that just generates output as a function of input are the most useful, and unit tests of code that just glues together a bunch of side effects are the least useful. I like to find ways to structure things so that it is mostly comprised of the first kind of code, and has so little logic in the second kind of code that it's easy to justify not unit testing it.
I once saw type checking presented as kind of an alternative to unit tests which doesn't rely on the assumption that developers write good tests, and I really like that viewpoint. A powerful type system can prevent many bugs, doesn't need additional test code and doesn't make any assumptions.
Of course it's not necessarily a full replacement, but it's definitely better and more time efficient than bad tests.
Yes, static analysis of types can eliminate a big class of unit tests (basically just checking pre- and post- conditions on types), which is a big part of why it's great. But most unit tests should be for logic. But that's why they aren't very useful for "glue" methods which just thread things through different dependencies - they shouldn't have much interesting logic.
> Of course it's not necessarily a full replacement
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
This is the way, but it’s tough to sell “we’re not going to unit test this part” in a professional setting where there are incentives to look more responsible than thou, directors are looking at unit test coverage reports, etc.
Yeah I have no qualms about writing mostly-BS unit tests when the existing structure of a project makes it infeasible to write good ones. When in Rome! But when I'm starting from scratch or have a chance to do refactoring, I move toward the "functional core" approach as much as possible.
You can still get code coverage points with broader tests.
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.”
I think my favorite of these was a function that called 4 other functions and didn't do anything else, its unit test mocked out all 4 other functions and asserted that each was called.
I've seen tests like that ("has the mock been called")... and in general I've felt that they were kinda useless.
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 would say it's worse than no test, because it discourages refactoring. Any alterations would make the test fail, even if it still does what it's supposed to do.
I've got mixed feelings about this. I'm leaning towards agreeing with you, because testing for the most part should be about the contract and not specific implementation. However in rare cases, as part of early development phase perhaps, I could see this being a useful signal. Provided it gets refactored later on.
I suppose having lots of mocks is a smell after all..
It’s better than no test because it makes the metric go up, and as we all know, virtuous and rational engineering practice is about recognizing that metrics are the objective truth about The Good and anything else is just your opinion. /s
The 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.
That’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.
I 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.
I'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).
The 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.
Not 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.
It used to be hard. I just spent a day in c# and gql figuring out how to do API level tests with a new framework. But when you get it working it's ever so much faster.
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.
Sorry, I probably wasn't clear. I wasn't spinning up the server itself, just testing the endpoints via their functions. Though this likely falls more under integration vs unit tests.
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 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.
The controller in this example is fooApi(). I generally reserve this layer for taking the input parameters (ie: query/post data), and then passing that data to some sort of service which executes the business logic on that data.
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.
I‘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.
Dependency 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.
I 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.
It 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
The 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.
No 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)...
Going 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:
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.
I 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.
The 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.
I 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.
I 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.
Agreed, 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.
+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.
Another 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.
Getting rid of data abstraction and encapsulation is throwing the baby with the bath water.
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 believe that combining state and functionality - the root of OOP - is a mistake. Tons of programming patterns and concepts exist to solve this fundamental mistake. When you stop using classes all of your code becomes so much cleaner, easier to reason about, test, debug, and so on. You can never create only pure functions in the real world but you get closer to this ideal.
I stand by my statement that OOP is totally unnecessary.
You can believe that the world is flat, announce that boldly and stand by your statement. It doesn't mean it's true in reality, only that it's true in your mind.
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.
OOP is successful despite of itself. My point is it’s just an inferior way of programming and there is no reason to use it other than legions of OOP programmers who take it as gospel. Obviously this is an opinion but it’s informed by lots of experience in new and legacy code bases.
To be blunt, you are stating "opinions" without any basis in facts; hence it is hard to take you seriously.
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.
The major reasons software exploded are the internet and the ubiquity of computing devices that get cheaper, faster, and smaller. Language had nothing to do with it, with or without OOP.
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.
>The major reasons software exploded are the internet and the ubiquity of computing devices that get cheaper, faster, and smaller. Language had nothing to do with it, with or without OOP.
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 as "Flat Earther" category and nothing more can be discussed.
The idea is to make the functions pure and return the state ‘a(s_1) -> s_2, value’. Makes so much easier to test and write concurrent programs.
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.
OOP does not have a monopoly on abstraction and encapsulation. What OOP does, namely object-level encapsulation, is just a very extreme way of structuring code around mutable state. IMO the better alternative is to avoid mutable state as much as possible and keep data and code separate. Code structured as pure functions is easy to test and encapsulation can be done at the module level.
OOP does not require mutable state. OOP has its origins in imperative languages, and so in the past OOP often involved poor management of mutable state.
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.
Recursion is an obfuscated way to run a loop with a stack. At first it seems like magic, and makes things so much easier. When trying to replicate it the first time actually using a loop and stack it’s really hard to get right. But by the time you’ve done it two or three times, it’s as easy as breathing.
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.
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.
I 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.
True, 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.
OOP tends to encourage thinking in more abstract ways than is often necessary imho. I feel like learning a language with only structs had a positive effect on my coding ability (as someone who more or less started with Java).
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. Nowadays I never use classes in Python, JS etc., it's just not needed - and in the case of Python it makes JSON insanely cumbersome.
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.